Madras1 commited on
Commit
5eabe6b
·
verified ·
1 Parent(s): d4d91b0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -37
app.py CHANGED
@@ -1,6 +1,7 @@
1
  # ==============================================================================
2
- # API do AetherMap — VERSÃO SÁBIA 5.2 (COMPLETA E SEGURA)
3
- # Backend com TODAS as funcionalidades originais + descrição de cluster por IA.
 
4
  # ==============================================================================
5
 
6
  import numpy as np
@@ -40,8 +41,7 @@ UMAP_N_NEIGHBORS = 30
40
  # A Câmara do Tesouro (Cache de Sessão)
41
  cache: Dict[str, Any] = {}
42
 
43
- # <<< NOVA SEÇÃO: INICIALIZAÇÃO SEGURA DO ORÁCULO >>>
44
- # Busca a chave dos segredos do Hugging Face ou do ambiente local
45
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
46
  try:
47
  if not GROQ_API_KEY:
@@ -52,7 +52,7 @@ except Exception as e:
52
  logging.error(f"FALHA CRÍTICA AO INICIALIZAR GROQ: {e}")
53
  groq_client = None
54
 
55
- # Palavras de Parada (mantidas do seu original)
56
  STOP_WORDS_PT = [
57
  'de','a','o','que','e','do','da','em','um','para','é','com','não','uma','os','no',
58
  'se','na','por','mais','as','dos','como','mas','foi','ao','ele','das','tem','à',
@@ -80,7 +80,7 @@ STOP_WORDS_PT = [
80
 
81
 
82
  # ==============================================================================
83
- # FUNÇÕES DE ANÁLISE (TODAS RESTAURADAS E INTACTAS)
84
  # ==============================================================================
85
  @lru_cache(maxsize=1)
86
  def load_model():
@@ -97,19 +97,14 @@ def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
97
  logging.info(f"Iniciando pipeline para {len(textos)} textos...")
98
  model = load_model()
99
  embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
100
-
101
  reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
102
  emb_3d = reducer.fit_transform(embeddings)
103
  emb_3d = StandardScaler().fit_transform(emb_3d)
104
-
105
- # <<< AJUSTE SUTIL E IMPORTANTE: HDBSCAN DINÂMICO >>>
106
  num_textos = len(textos)
107
- min_size = max(10, int(num_textos * 0.03))
108
  logging.info(f"HDBSCAN min_cluster_size dinâmico definido para: {min_size}")
109
-
110
  clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
111
  clusters = clusterer.fit_predict(emb_3d)
112
-
113
  df = pd.DataFrame({"x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2], "full_text": textos, "cluster": clusters.astype(str)})
114
  del reducer, clusterer, emb_3d; gc.collect()
115
  return df, embeddings
@@ -124,15 +119,12 @@ def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
124
  tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
125
  except ValueError:
126
  return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "entropia": 0.0}
127
-
128
  vocab_count = vectorizer_count.get_feature_names_out()
129
  contagens = counts_matrix.sum(axis=0).A1
130
-
131
  vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
132
  soma_tfidf = tfidf_matrix.sum(axis=0).A1
133
  top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
134
  top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
135
-
136
  return {
137
  "riqueza_lexical": len(vocab_count),
138
  "top_tfidf_palavras": top_tfidf,
@@ -149,16 +141,11 @@ def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str,
149
  triu_indices = np.triu_indices_from(sim, k=1)
150
  sim_vetor = sim[triu_indices]
151
  pares_idx = np.where(sim_vetor > 0.98)[0]
152
- # Pegamos os 5 mais similares para não sobrecarregar
153
  top_pares_idx = pares_idx[np.argsort(sim_vetor[pares_idx])[-5:][::-1]]
154
  for i in top_pares_idx:
155
  idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
156
  if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
157
- pares_semanticos.append({
158
- "similaridade": float(sim[idx1, idx2]),
159
- "texto1": df["full_text"].iloc[idx1],
160
- "texto2": df["full_text"].iloc[idx2]
161
- })
162
  return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
163
 
164
  def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
@@ -180,10 +167,11 @@ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
180
  analise[cid] = {"num_documentos": len(textos_cluster), "top_palavras": top_palavras}
181
  return analise
182
 
 
183
  # ==============================================================================
184
  # FASTAPI — DEFINIÇÃO DA API (COMPLETA)
185
  # ==============================================================================
186
- app = FastAPI(title="API do AetherMap (Versão Sábia e Completa)", version="5.2.0")
187
 
188
  @app.post("/process/")
189
  async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
@@ -199,7 +187,6 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
199
  cache[job_id] = {"embeddings": embeddings, "df": df}
200
  logging.info(f"Resultados salvos no cache com o ID: {job_id}")
201
 
202
- # TODAS AS ANÁLISES SENDO FEITAS E RETORNADAS
203
  metricas_globais = calcular_metricas(df["full_text"].tolist())
204
  analise_de_duplicados = encontrar_duplicados(df, embeddings)
205
  analise_por_cluster_tfidf = analisar_clusters(df)
@@ -220,21 +207,68 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
220
  raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {str(e)}")
221
 
222
 
223
- # Endpoint de Busca Semântica (INTACTO)
224
  @app.post("/search/")
225
  async def search_api(query: str = Form(...), job_id: str = Form(...)):
226
- # ... (código do /search/ inalterado)
227
- if job_id not in cache: raise HTTPException(status_code=404, detail="Job ID não encontrado ou expirado.")
228
- model = load_model()
229
- cached_data = cache[job_id]
230
- corpus_embeddings = cached_data["embeddings"]
231
- query_embedding = model.encode([query], convert_to_numpy=True)
232
- similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
233
- top_indices = np.argsort(similarities)[-100:][::-1]
234
- results = [{"index": int(i), "score": float(similarities[i])} for i in top_indices if similarities[i] > 0.3]
235
- return {"results": results}
236
-
237
- # <<< NOVO FEITIÇO IMPERIAL, ACOPLADO AO REINO >>>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  @app.post("/describe_clusters/")
239
  async def describe_clusters_api(job_id: str = Form(...)):
240
  logging.info(f"Requisição recebida para descrever clusters do job '{job_id}'.")
@@ -277,13 +311,14 @@ async def describe_clusters_api(job_id: str = Form(...)):
277
  messages=[
278
  {"role": "system", "content": "Siga as instruções e responda apenas com um objeto JSON válido."},
279
  {"role": "user", "content": master_prompt},
280
- ], model="meta-llama/llama-4-scout-17b-16e-instruct", temperature=0.2,
281
  )
282
  response_content = chat_completion.choices[0].message.content
283
 
284
  try:
285
  insights = json.loads(response_content.strip().replace("```json", "").replace("```", ""))
286
  except json.JSONDecodeError:
 
287
  raise HTTPException(status_code=500, detail="O Oráculo respondeu em um formato inesperado.")
288
 
289
  return {"insights": insights}
 
1
  # ==============================================================================
2
+ # API do AetherMap — VERSÃO SÁBIA 6.0 (O SÁBIO INVOCADO)
3
+ # Backend com RAG (Retrieval-Augmented Generation) na busca semântica.
4
+ # Todas as funcionalidades anteriores estão presentes e aprimoradas.
5
  # ==============================================================================
6
 
7
  import numpy as np
 
41
  # A Câmara do Tesouro (Cache de Sessão)
42
  cache: Dict[str, Any] = {}
43
 
44
+ # Inicialização segura do Oráculo Groq
 
45
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
46
  try:
47
  if not GROQ_API_KEY:
 
52
  logging.error(f"FALHA CRÍTICA AO INICIALIZAR GROQ: {e}")
53
  groq_client = None
54
 
55
+ # Palavras de Parada
56
  STOP_WORDS_PT = [
57
  'de','a','o','que','e','do','da','em','um','para','é','com','não','uma','os','no',
58
  'se','na','por','mais','as','dos','como','mas','foi','ao','ele','das','tem','à',
 
80
 
81
 
82
  # ==============================================================================
83
+ # FUNÇÕES DE ANÁLISE (INTACTAS DA VERSÃO COMPLETA)
84
  # ==============================================================================
85
  @lru_cache(maxsize=1)
86
  def load_model():
 
97
  logging.info(f"Iniciando pipeline para {len(textos)} textos...")
98
  model = load_model()
99
  embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
 
100
  reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
101
  emb_3d = reducer.fit_transform(embeddings)
102
  emb_3d = StandardScaler().fit_transform(emb_3d)
 
 
103
  num_textos = len(textos)
104
+ min_size = max(10, int(num_textos * 0.02))
105
  logging.info(f"HDBSCAN min_cluster_size dinâmico definido para: {min_size}")
 
106
  clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
107
  clusters = clusterer.fit_predict(emb_3d)
 
108
  df = pd.DataFrame({"x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2], "full_text": textos, "cluster": clusters.astype(str)})
109
  del reducer, clusterer, emb_3d; gc.collect()
110
  return df, embeddings
 
119
  tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
120
  except ValueError:
121
  return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "entropia": 0.0}
 
122
  vocab_count = vectorizer_count.get_feature_names_out()
123
  contagens = counts_matrix.sum(axis=0).A1
 
124
  vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
125
  soma_tfidf = tfidf_matrix.sum(axis=0).A1
126
  top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
127
  top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
 
128
  return {
129
  "riqueza_lexical": len(vocab_count),
130
  "top_tfidf_palavras": top_tfidf,
 
141
  triu_indices = np.triu_indices_from(sim, k=1)
142
  sim_vetor = sim[triu_indices]
143
  pares_idx = np.where(sim_vetor > 0.98)[0]
 
144
  top_pares_idx = pares_idx[np.argsort(sim_vetor[pares_idx])[-5:][::-1]]
145
  for i in top_pares_idx:
146
  idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
147
  if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
148
+ pares_semanticos.append({"similaridade": float(sim[idx1, idx2]), "texto1": df["full_text"].iloc[idx1], "texto2": df["full_text"].iloc[idx2]})
 
 
 
 
149
  return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
150
 
151
  def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
 
167
  analise[cid] = {"num_documentos": len(textos_cluster), "top_palavras": top_palavras}
168
  return analise
169
 
170
+
171
  # ==============================================================================
172
  # FASTAPI — DEFINIÇÃO DA API (COMPLETA)
173
  # ==============================================================================
174
+ app = FastAPI(title="API do AetherMap (O Sábio Invocado)", version="6.0.0")
175
 
176
  @app.post("/process/")
177
  async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
 
187
  cache[job_id] = {"embeddings": embeddings, "df": df}
188
  logging.info(f"Resultados salvos no cache com o ID: {job_id}")
189
 
 
190
  metricas_globais = calcular_metricas(df["full_text"].tolist())
191
  analise_de_duplicados = encontrar_duplicados(df, embeddings)
192
  analise_por_cluster_tfidf = analisar_clusters(df)
 
207
  raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {str(e)}")
208
 
209
 
 
210
  @app.post("/search/")
211
  async def search_api(query: str = Form(...), job_id: str = Form(...)):
212
+ logging.info(f"Busca RAG recebida para a query '{query}' no job '{job_id}'.")
213
+ if job_id not in cache:
214
+ raise HTTPException(status_code=404, detail="Job ID não encontrado ou expirado.")
215
+
216
+ try:
217
+ # ETAPA 1: RECUPERAÇÃO (RETRIEVAL)
218
+ model = load_model()
219
+ cached_data = cache[job_id]
220
+ df = cached_data["df"]
221
+ corpus_embeddings = cached_data["embeddings"]
222
+
223
+ query_embedding = model.encode([query], convert_to_numpy=True)
224
+ similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
225
+
226
+ top_k = 10
227
+ top_indices = np.argsort(similarities)[-top_k:][::-1]
228
+
229
+ results = [
230
+ {"index": int(i), "score": float(similarities[i])}
231
+ for i in top_indices if similarities[i] > 0.3
232
+ ]
233
+
234
+ if not results:
235
+ return {"summary": "Não foram encontrados resultados relevantes para sua busca.", "results": []}
236
+
237
+ # ETAPA 2: GERAÇÃO AUMENTADA (AUGMENTED GENERATION)
238
+ summary = ""
239
+ if groq_client:
240
+ context_docs = [df.iloc[res["index"]]["full_text"] for res in results[:5]]
241
+ context_str = "\n\n".join([f"Documento de referência {i+1}:\n'''{doc}'''" for i, doc in enumerate(context_docs)])
242
+
243
+ rag_prompt = (
244
+ "Você é Aetherius, um Sábio Conselheiro. Sua tarefa é responder à pergunta do usuário de forma concisa e direta, "
245
+ "baseando-se **estritamente** nas informações contidas nos documentos de referência fornecidos. "
246
+ "Não use nenhum conhecimento externo. Responda em uma ou duas frases.\n\n"
247
+ f"**Pergunta do Usuário:** \"{query}\"\n\n"
248
+ f"{context_str}\n\n"
249
+ "**Sua Resposta Direta:**"
250
+ )
251
+
252
+ try:
253
+ chat_completion = groq_client.chat.completions.create(
254
+ messages=[{"role": "user", "content": rag_prompt}],
255
+ model="llama3-8b-8192",
256
+ temperature=0.2,
257
+ )
258
+ summary = chat_completion.choices[0].message.content.strip()
259
+ logging.info(f"Resumo RAG gerado com sucesso.")
260
+ except Exception as e:
261
+ logging.warning(f"Falha ao gerar resumo RAG com a Groq: {e}")
262
+ summary = "O Oráculo está indisponível para gerar um resumo, mas aqui estão os documentos encontrados."
263
+
264
+ logging.info(f"Encontrados {len(results)} resultados. Resumo: {summary[:50]}...")
265
+ return {"summary": summary, "results": results}
266
+
267
+ except Exception as e:
268
+ logging.error(f"ERRO CRÍTICO EM /search/: {e}", exc_info=True)
269
+ raise HTTPException(status_code=500, detail=f"Erro interno na busca semântica: {str(e)}")
270
+
271
+
272
  @app.post("/describe_clusters/")
273
  async def describe_clusters_api(job_id: str = Form(...)):
274
  logging.info(f"Requisição recebida para descrever clusters do job '{job_id}'.")
 
311
  messages=[
312
  {"role": "system", "content": "Siga as instruções e responda apenas com um objeto JSON válido."},
313
  {"role": "user", "content": master_prompt},
314
+ ], model="llama3-8b-8192", temperature=0.2,
315
  )
316
  response_content = chat_completion.choices[0].message.content
317
 
318
  try:
319
  insights = json.loads(response_content.strip().replace("```json", "").replace("```", ""))
320
  except json.JSONDecodeError:
321
+ logging.error(f"Falha ao decodificar JSON da Groq. Resposta: {response_content}")
322
  raise HTTPException(status_code=500, detail="O Oráculo respondeu em um formato inesperado.")
323
 
324
  return {"insights": insights}