Ana2012 commited on
Commit
90a7c8d
·
verified ·
1 Parent(s): 50136de

Update app/memory.py

Browse files
Files changed (1) hide show
  1. app/memory.py +75 -261
app/memory.py CHANGED
@@ -1,268 +1,82 @@
 
1
  import os
 
 
 
2
 
3
- import numpy as np
4
- import pandas as pd
5
- import torch
6
- from sentence_transformers import SentenceTransformer
7
- from sklearn.metrics.pairwise import cosine_similarity
8
 
9
- from .memory import caminho_memoria_negativa
10
- from .utils import (
11
- bonus_lexical,
12
- inferir_categoria_consulta,
13
- limpar_texto,
14
- mapear_categoria,
15
- )
16
 
17
 
18
- BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
- DATA_DIR = os.path.join(BASE_DIR, "data")
20
- PATH_PRODUCTS = os.path.join(DATA_DIR, "produtos_finetunado.csv")
21
- PATH_EMBEDDINGS = os.path.join(DATA_DIR, "embeddings_produtos_finetunado.npy")
22
-
23
- MODEL_NAME = os.getenv("HF_MODEL_REPO", "Ana2012/bertimbau-buscador").strip()
24
- HF_API_TOKEN = os.getenv("HF_API_TOKEN", "").strip()
25
-
26
-
27
- class SearchEngine:
28
- def __init__(self):
29
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
30
- self.model = None
31
- self.df_produtos = None
32
- self.emb_produtos = None
33
- self.df_negative_memory = pd.DataFrame()
34
- self.negative_memory_mtime = None
35
-
36
- def load(self):
37
- self._load_products()
38
- self._load_model()
39
- self._load_embeddings()
40
- self._refresh_negative_memory(force=True)
41
-
42
- def _load_products(self):
43
- df = pd.read_csv(PATH_PRODUCTS)
44
- df.columns = df.columns.str.strip().str.lower()
45
-
46
- df["product_name"] = df["product_name"].fillna("").astype(str)
47
- df["description"] = df["description"].fillna("").astype(str)
48
- df["categoria_principal"] = df["categoria_principal"].fillna("").astype(str)
49
- df["category_names_text"] = df["category_names_text"].fillna("").astype(str)
50
- df["region"] = df["region"].fillna("").astype(str)
51
- df["neighborhood"] = df["neighborhood"].fillna("").astype(str)
52
-
53
- df["product_name_limpo"] = df["product_name"].apply(limpar_texto)
54
- df["description_limpa"] = df["description"].apply(limpar_texto)
55
- df["categoria_principal_limpa"] = df["categoria_principal"].apply(limpar_texto)
56
- df["category_names_text_limpo"] = df["category_names_text"].apply(limpar_texto)
57
- df["region_limpa"] = df["region"].apply(limpar_texto)
58
- df["neighborhood_limpo"] = df["neighborhood"].apply(limpar_texto)
59
-
60
- df["texto_busca_reforcado"] = (
61
- "produto " + df["product_name_limpo"] + " "
62
- + "categoria " + df["categoria_principal_limpa"] + " "
63
- + "categorias " + df["category_names_text_limpo"] + " "
64
- + "bairro " + df["neighborhood_limpo"] + " "
65
- + "regiao " + df["region_limpa"] + " "
66
- + "descricao " + df["description_limpa"]
67
- ).str.strip()
68
-
69
- df["categoria_grupo"] = df["categoria_principal"].apply(mapear_categoria)
70
-
71
- self.df_produtos = df
72
-
73
- def _load_model(self):
74
- kwargs = {"device": self.device}
75
- if HF_API_TOKEN:
76
- kwargs["token"] = HF_API_TOKEN
77
-
78
- self.model = SentenceTransformer(MODEL_NAME, **kwargs)
79
-
80
- def _load_embeddings(self):
81
- self.emb_produtos = np.load(PATH_EMBEDDINGS)
82
-
83
- if self.emb_produtos.ndim != 2:
84
- raise RuntimeError("O arquivo de embeddings precisa conter uma matriz 2D.")
85
-
86
- def runtime_info(self):
87
- return {
88
- "model_repo": MODEL_NAME,
89
- "device": self.device,
90
- "products_loaded": 0 if self.df_produtos is None else int(len(self.df_produtos)),
91
- "embeddings_loaded": 0 if self.emb_produtos is None else int(len(self.emb_produtos)),
92
- "embedding_dim": 0 if self.emb_produtos is None else int(self.emb_produtos.shape[1]),
93
- }
94
-
95
- def _refresh_negative_memory(self, force=False):
96
- memory_path = caminho_memoria_negativa()
97
-
98
- if not os.path.exists(memory_path):
99
- self.df_negative_memory = pd.DataFrame()
100
- self.negative_memory_mtime = None
101
- return
102
-
103
- current_mtime = os.path.getmtime(memory_path)
104
- if not force and self.negative_memory_mtime == current_mtime:
105
- return
106
 
 
107
  try:
108
- df = pd.read_csv(memory_path)
109
- except Exception:
110
- self.df_negative_memory = pd.DataFrame()
111
- self.negative_memory_mtime = current_mtime
112
- return
113
-
114
- df.columns = df.columns.str.strip().str.lower()
115
-
116
- for col in ["query", "product_id", "product_name", "motivo", "rating"]:
117
- if col not in df.columns:
118
- df[col] = ""
119
-
120
- df["query"] = df["query"].fillna("").astype(str)
121
- df["query_limpa"] = df["query"].apply(limpar_texto)
122
- df["product_id"] = df["product_id"].fillna("").astype(str)
123
- df["product_name"] = df["product_name"].fillna("").astype(str)
124
- df["motivo"] = df["motivo"].fillna("").astype(str)
125
- df["rating_num"] = pd.to_numeric(df["rating"], errors="coerce")
126
-
127
- self.df_negative_memory = df
128
- self.negative_memory_mtime = current_mtime
129
-
130
- def _similaridade_consulta(self, query_atual, query_memoria):
131
- if not query_atual or not query_memoria:
132
- return 0.0
133
-
134
- if query_atual == query_memoria:
135
- return 1.0
136
-
137
- termos_atuais = set(query_atual.split())
138
- termos_memoria = set(query_memoria.split())
139
-
140
- if not termos_atuais or not termos_memoria:
141
- return 0.0
142
-
143
- intersecao = len(termos_atuais & termos_memoria)
144
- if intersecao == 0:
145
- return 0.0
146
-
147
- return intersecao / max(len(termos_atuais), len(termos_memoria))
148
-
149
- def _calcular_penalidade_feedback(self, query_text, df_filtrado):
150
- self._refresh_negative_memory()
151
-
152
- if self.df_negative_memory.empty:
153
- return np.zeros(len(df_filtrado))
154
-
155
- query_limpa = limpar_texto(query_text)
156
- memorias = self.df_negative_memory[
157
- self.df_negative_memory["product_id"].isin(df_filtrado["product_id"].astype(str))
158
- ]
159
-
160
- if memorias.empty:
161
- return np.zeros(len(df_filtrado))
162
-
163
- penalidades = {}
164
-
165
- for _, memoria in memorias.iterrows():
166
- similaridade = self._similaridade_consulta(query_limpa, memoria["query_limpa"])
167
- if similaridade <= 0:
168
- continue
169
-
170
- penalidade = 0.08 + (0.12 * similaridade)
171
-
172
- if memoria["motivo"] == "nao_foi_util":
173
- penalidade += 0.04
174
-
175
- if pd.notna(memoria["rating_num"]) and memoria["rating_num"] <= 2:
176
- penalidade += 0.04
177
-
178
- product_id = memoria["product_id"]
179
- penalidades[product_id] = min(
180
- penalidades.get(product_id, 0.0) + penalidade,
181
- 0.45,
182
- )
183
-
184
- return df_filtrado["product_id"].astype(str).map(
185
- lambda x: penalidades.get(x, 0.0)
186
- ).values
187
-
188
- def gerar_embedding_unico(self, texto):
189
- embedding = self.model.encode(
190
- texto,
191
- convert_to_numpy=True,
192
- normalize_embeddings=False,
193
- show_progress_bar=False,
194
- )
195
- return np.asarray(embedding, dtype=np.float32)
196
-
197
- def buscar(self, query_text, top_k=5):
198
- top_k = max(1, int(top_k or 5))
199
-
200
- query_limpa = limpar_texto(query_text)
201
- categoria = inferir_categoria_consulta(query_limpa)
202
-
203
- if categoria is not None:
204
- mask = self.df_produtos["categoria_grupo"] == categoria
205
- df_filtrado = self.df_produtos[mask].copy()
206
- idx_filtrado = df_filtrado.index.tolist()
207
- else:
208
- df_filtrado = self.df_produtos.copy()
209
- idx_filtrado = df_filtrado.index.tolist()
210
-
211
- if len(df_filtrado) == 0:
212
- df_filtrado = self.df_produtos.copy()
213
- idx_filtrado = df_filtrado.index.tolist()
214
-
215
- emb_query = self.gerar_embedding_unico(query_text).reshape(1, -1)
216
- emb_base = self.emb_produtos[idx_filtrado]
217
-
218
- if emb_base.shape[1] != emb_query.shape[1]:
219
- raise RuntimeError(
220
- "Dimensao incompatível entre os embeddings salvos e o embedding da consulta. "
221
- "Regenere o arquivo .npy com o mesmo modelo SentenceTransformer."
222
- )
223
-
224
- sims = cosine_similarity(emb_query, emb_base)[0]
225
-
226
- bonus = df_filtrado.apply(
227
- lambda row: bonus_lexical(
228
- query_text,
229
- row["product_name"],
230
- row["categoria_principal"],
231
- row["neighborhood"],
232
- row["region"],
233
- row["description"],
234
- row["texto_busca_reforcado"],
235
- ),
236
- axis=1,
237
- ).values
238
-
239
- penalidade_feedback = self._calcular_penalidade_feedback(query_text, df_filtrado)
240
- score_final = sims + bonus - penalidade_feedback
241
-
242
- top_idx_local = np.argsort(score_final)[::-1][:top_k]
243
-
244
- resultados = []
245
- for rank, idx_local in enumerate(top_idx_local, start=1):
246
- idx_global = idx_filtrado[idx_local]
247
- prod = self.df_produtos.iloc[idx_global]
248
-
249
- resultados.append({
250
- "rank": rank,
251
- "establishment_id": str(prod["establishment_id"]),
252
- "product_id": str(prod["product_id"]),
253
- "product_name": prod["product_name"],
254
- "categoria_principal": prod["categoria_principal"],
255
- "categoria_grupo": prod["categoria_grupo"],
256
- "region": prod["region"],
257
- "neighborhood": prod["neighborhood"],
258
- "score_semantico": float(sims[idx_local]),
259
- "bonus_lexical": float(bonus[idx_local]),
260
- "penalidade_feedback": float(penalidade_feedback[idx_local]),
261
- "score_final": float(score_final[idx_local]),
262
- })
263
-
264
- return {
265
- "query": query_text,
266
- "categoria_inferida": categoria,
267
- "resultados": resultados,
268
- }
 
1
+ import csv
2
  import os
3
+ import tempfile
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
 
 
 
 
 
 
7
 
8
+ DEFAULT_LOGS_DIR = Path(os.getenv("LOGS_DIR", "/data/logs"))
9
+ FALLBACK_LOGS_DIR = Path(tempfile.gettempdir()) / "tcc2_agent" / "logs"
 
 
 
 
 
10
 
11
 
12
+ def _resolve_logs_dir():
13
+ preferred_dir = os.environ.get("LOGS_DIR", "").strip()
14
+ candidates = [Path(preferred_dir)] if preferred_dir else []
15
+ candidates.extend([DEFAULT_LOGS_DIR, FALLBACK_LOGS_DIR])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ for directory in candidates:
18
  try:
19
+ directory.mkdir(parents=True, exist_ok=True)
20
+ return directory
21
+ except OSError:
22
+ continue
23
+
24
+ raise OSError(
25
+ "Nao foi possivel criar um diretorio para armazenar a memoria negativa. "
26
+ "Defina LOGS_DIR para um caminho gravavel."
27
+ )
28
+
29
+
30
+ def caminho_memoria_negativa():
31
+ return str(_resolve_logs_dir() / "negative_memory.csv")
32
+
33
+
34
+ def inicializar_memoria_negativa():
35
+ memory_file = caminho_memoria_negativa()
36
+
37
+ if not os.path.exists(memory_file):
38
+ with open(memory_file, "w", newline="", encoding="utf-8") as f:
39
+ writer = csv.writer(f)
40
+ writer.writerow([
41
+ "timestamp",
42
+ "search_id",
43
+ "rank",
44
+ "query",
45
+ "product_id",
46
+ "product_name",
47
+ "rating",
48
+ "motivo",
49
+ ])
50
+
51
+
52
+ def salvar_memoria_negativa(
53
+ query,
54
+ product_id,
55
+ product_name,
56
+ rating,
57
+ motivo="feedback_negativo",
58
+ search_id=None,
59
+ rank=None,
60
+ ):
61
+ inicializar_memoria_negativa()
62
+ memory_file = caminho_memoria_negativa()
63
+
64
+ row = [
65
+ datetime.now(timezone.utc).isoformat(),
66
+ search_id or "",
67
+ rank if rank is not None else "",
68
+ query or "",
69
+ product_id or "",
70
+ product_name or "",
71
+ rating if rating is not None else "",
72
+ motivo or "feedback_negativo",
73
+ ]
74
+
75
+ try:
76
+ with open(memory_file, "a", newline="", encoding="utf-8") as f:
77
+ writer = csv.writer(f)
78
+ writer.writerow(row)
79
+ except Exception as e:
80
+ print("Erro ao salvar memoria negativa:", e)
81
+
82
+ return {"status": "ok"}