Ana2012 commited on
Commit
0bcc792
·
verified ·
1 Parent(s): 33203e9

Upload 11 files

Browse files
Files changed (3) hide show
  1. app/feedback.py +131 -38
  2. app/google_oauth.py +144 -0
  3. app/main.py +238 -186
app/feedback.py CHANGED
@@ -1,15 +1,31 @@
1
  import csv
2
  import os
3
- from datetime import datetime
4
 
 
5
  from .memory import salvar_memoria_negativa
6
-
7
-
8
- BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
- # Usa volume persistente do Fly.io montado em /data
10
- # Garante que feedback não seja perdido após deploy/restart
11
- LOGS_DIR = os.getenv("LOGS_DIR", "/data/logs")
12
- FEEDBACK_FILE = os.path.join(LOGS_DIR, "feedback.csv")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
  def garantir_pasta_logs():
@@ -22,38 +38,111 @@ def inicializar_arquivo_feedback():
22
  if not os.path.exists(FEEDBACK_FILE):
23
  with open(FEEDBACK_FILE, mode="w", newline="", encoding="utf-8") as f:
24
  writer = csv.writer(f)
25
- writer.writerow([
26
- "timestamp",
27
- "query",
28
- "product_id",
29
- "product_name",
30
- "rating",
31
- "is_helpful"
32
- ])
33
 
34
 
35
- def salvar_feedback(query, product_id, product_name, rating=None, is_helpful=None):
36
- inicializar_arquivo_feedback()
37
 
38
  with open(FEEDBACK_FILE, mode="a", newline="", encoding="utf-8") as f:
39
  writer = csv.writer(f)
40
- writer.writerow([
41
- datetime.now().isoformat(),
42
- query,
43
- product_id,
44
- product_name,
45
- rating if rating is not None else "",
46
- is_helpful if is_helpful is not None else ""
47
- ])
48
-
49
- # Regra simples para criar memória negativa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  if rating is not None and rating <= 2:
51
  salvar_memoria_negativa(
52
  query=query,
53
  product_id=product_id,
54
  product_name=product_name,
55
  rating=rating,
56
- motivo="rating_baixo"
57
  )
58
 
59
  if is_helpful is False:
@@ -62,14 +151,18 @@ def salvar_feedback(query, product_id, product_name, rating=None, is_helpful=Non
62
  product_id=product_id,
63
  product_name=product_name,
64
  rating=rating if rating is not None else "",
65
- motivo="nao_foi_util"
66
  )
67
 
68
- return {
69
- "status": "ok",
70
- "message": "Feedback salvo com sucesso."
71
- }
72
-
73
-
74
- def caminho_feedback():
75
- return FEEDBACK_FILE
 
 
 
 
 
1
  import csv
2
  import os
3
+ from datetime import datetime, timezone
4
 
5
+ from .google_oauth import GoogleAuthRequiredError, append_feedback_to_sheet, load_credentials
6
  from .memory import salvar_memoria_negativa
7
+
8
+
9
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+ LOGS_DIR = os.getenv("LOGS_DIR", "/data/logs")
11
+ FEEDBACK_FILE = os.path.join(LOGS_DIR, "feedback.csv")
12
+ FEEDBACK_HEADERS = [
13
+ "timestamp",
14
+ "query",
15
+ "product_id",
16
+ "product_name",
17
+ "rating",
18
+ "is_helpful",
19
+ "note",
20
+ "categoria_inferida",
21
+ "feedback",
22
+ "motivo",
23
+ "score_final",
24
+ "score_semantico",
25
+ "bonus_lexical",
26
+ "penalidade_feedback",
27
+ "user_message",
28
+ ]
29
 
30
 
31
  def garantir_pasta_logs():
 
38
  if not os.path.exists(FEEDBACK_FILE):
39
  with open(FEEDBACK_FILE, mode="w", newline="", encoding="utf-8") as f:
40
  writer = csv.writer(f)
41
+ writer.writerow(FEEDBACK_HEADERS)
42
+
43
+
44
+ def google_sheets_habilitado():
45
+ return load_credentials() is not None and bool(os.getenv("GOOGLE_SPREADSHEET_ID", "").strip())
 
 
 
46
 
47
 
48
+ def _append_feedback_csv(row):
49
+ inicializar_arquivo_feedback()
50
 
51
  with open(FEEDBACK_FILE, mode="a", newline="", encoding="utf-8") as f:
52
  writer = csv.writer(f)
53
+ writer.writerow([row.get(header, "") for header in FEEDBACK_HEADERS])
54
+
55
+
56
+ def _build_feedback_payload(
57
+ query,
58
+ product_id,
59
+ product_name,
60
+ rating=None,
61
+ is_helpful=None,
62
+ note=None,
63
+ categoria_inferida=None,
64
+ feedback=None,
65
+ motivo=None,
66
+ score_final=None,
67
+ score_semantico=None,
68
+ bonus_lexical=None,
69
+ penalidade_feedback=None,
70
+ user_message=None,
71
+ ):
72
+ return {
73
+ "timestamp": datetime.now(timezone.utc).isoformat(),
74
+ "query": query,
75
+ "product_id": product_id,
76
+ "product_name": product_name,
77
+ "rating": rating if rating is not None else "",
78
+ "is_helpful": is_helpful if is_helpful is not None else "",
79
+ "note": note or "",
80
+ "categoria_inferida": categoria_inferida or "",
81
+ "feedback": feedback or "",
82
+ "motivo": motivo or "",
83
+ "score_final": score_final if score_final is not None else "",
84
+ "score_semantico": score_semantico if score_semantico is not None else "",
85
+ "bonus_lexical": bonus_lexical if bonus_lexical is not None else "",
86
+ "penalidade_feedback": penalidade_feedback if penalidade_feedback is not None else "",
87
+ "user_message": user_message or "",
88
+ }
89
+
90
+
91
+ def salvar_feedback(
92
+ query,
93
+ product_id,
94
+ product_name,
95
+ rating=None,
96
+ is_helpful=None,
97
+ note=None,
98
+ categoria_inferida=None,
99
+ feedback=None,
100
+ motivo=None,
101
+ score_final=None,
102
+ score_semantico=None,
103
+ bonus_lexical=None,
104
+ penalidade_feedback=None,
105
+ user_message=None,
106
+ ):
107
+ row = _build_feedback_payload(
108
+ query=query,
109
+ product_id=product_id,
110
+ product_name=product_name,
111
+ rating=rating,
112
+ is_helpful=is_helpful,
113
+ note=note,
114
+ categoria_inferida=categoria_inferida,
115
+ feedback=feedback,
116
+ motivo=motivo,
117
+ score_final=score_final,
118
+ score_semantico=score_semantico,
119
+ bonus_lexical=bonus_lexical,
120
+ penalidade_feedback=penalidade_feedback,
121
+ user_message=user_message,
122
+ )
123
+
124
+ _append_feedback_csv(row)
125
+ saved_google_sheets = False
126
+ warning = None
127
+
128
+ try:
129
+ append_feedback_to_sheet(row)
130
+ saved_google_sheets = True
131
+ except GoogleAuthRequiredError:
132
+ warning = "Feedback salvo localmente, mas nao enviado ao Google Sheets. Acesse /auth/google para autorizar."
133
+ except Exception as exc:
134
+ warning = (
135
+ "Feedback salvo localmente, mas nao enviado ao Google Sheets. "
136
+ f"Erro de sincronizacao: {exc}"
137
+ )
138
+
139
  if rating is not None and rating <= 2:
140
  salvar_memoria_negativa(
141
  query=query,
142
  product_id=product_id,
143
  product_name=product_name,
144
  rating=rating,
145
+ motivo="rating_baixo",
146
  )
147
 
148
  if is_helpful is False:
 
151
  product_id=product_id,
152
  product_name=product_name,
153
  rating=rating if rating is not None else "",
154
+ motivo="nao_foi_util",
155
  )
156
 
157
+ response = {
158
+ "ok": True,
159
+ "saved_local": True,
160
+ "saved_google_sheets": saved_google_sheets,
161
+ }
162
+ if warning:
163
+ response["warning"] = warning
164
+ return response
165
+
166
+
167
+ def caminho_feedback():
168
+ return FEEDBACK_FILE
app/google_oauth.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from google.auth.transport.requests import Request
6
+ from google.oauth2.credentials import Credentials
7
+ from google_auth_oauthlib.flow import Flow
8
+ from googleapiclient.discovery import build
9
+
10
+ SCOPES = [
11
+ "https://www.googleapis.com/auth/spreadsheets",
12
+ "https://www.googleapis.com/auth/drive.file",
13
+ ]
14
+ TOKEN_FILENAME = "google_token.json"
15
+ DATA_DIR = Path("/data")
16
+
17
+
18
+ class GoogleOAuthError(RuntimeError):
19
+ pass
20
+
21
+
22
+ class GoogleAuthRequiredError(GoogleOAuthError):
23
+ pass
24
+
25
+
26
+ def get_token_path():
27
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
28
+ return DATA_DIR / TOKEN_FILENAME
29
+
30
+
31
+ def _client_config():
32
+ client_id = os.environ.get("GOOGLE_CLIENT_ID", "").strip()
33
+ client_secret = os.environ.get("GOOGLE_CLIENT_SECRET", "").strip()
34
+ redirect_uri = os.environ.get("GOOGLE_REDIRECT_URI", "").strip()
35
+
36
+ missing = [
37
+ name
38
+ for name, value in {
39
+ "GOOGLE_CLIENT_ID": client_id,
40
+ "GOOGLE_CLIENT_SECRET": client_secret,
41
+ "GOOGLE_REDIRECT_URI": redirect_uri,
42
+ }.items()
43
+ if not value
44
+ ]
45
+ if missing:
46
+ raise GoogleOAuthError("Variaveis OAuth ausentes: " + ", ".join(missing))
47
+
48
+ return {
49
+ "web": {
50
+ "client_id": client_id,
51
+ "client_secret": client_secret,
52
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
53
+ "token_uri": "https://oauth2.googleapis.com/token",
54
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
55
+ "redirect_uris": [redirect_uri],
56
+ }
57
+ }
58
+
59
+
60
+ def build_flow(state=None):
61
+ redirect_uri = os.environ.get("GOOGLE_REDIRECT_URI", "").strip()
62
+ flow = Flow.from_client_config(_client_config(), scopes=SCOPES, state=state)
63
+ flow.redirect_uri = redirect_uri
64
+ return flow
65
+
66
+
67
+ def get_authorization_url():
68
+ flow = build_flow()
69
+ authorization_url, state = flow.authorization_url(
70
+ access_type="offline",
71
+ prompt="consent",
72
+ include_granted_scopes="true",
73
+ )
74
+ return authorization_url, state
75
+
76
+
77
+ def save_credentials(credentials):
78
+ token_path = get_token_path()
79
+ token_path.write_text(credentials.to_json(), encoding="utf-8")
80
+
81
+
82
+ def load_credentials():
83
+ token_path = get_token_path()
84
+ if not token_path.exists():
85
+ return None
86
+
87
+ try:
88
+ payload = json.loads(token_path.read_text(encoding="utf-8"))
89
+ credentials = Credentials.from_authorized_user_info(payload, SCOPES)
90
+ except Exception:
91
+ return None
92
+
93
+ if credentials.expired and credentials.refresh_token:
94
+ try:
95
+ credentials.refresh(Request())
96
+ save_credentials(credentials)
97
+ except Exception:
98
+ return None
99
+
100
+ if not credentials.valid:
101
+ return None
102
+
103
+ return credentials
104
+
105
+
106
+ def get_sheets_service():
107
+ credentials = load_credentials()
108
+ if credentials is None:
109
+ raise GoogleAuthRequiredError(
110
+ "Google Sheets nao autorizado. Acesse /auth/google primeiro."
111
+ )
112
+
113
+ return build("sheets", "v4", credentials=credentials, cache_discovery=False)
114
+
115
+
116
+ def append_feedback_to_sheet(feedback_data):
117
+ spreadsheet_id = os.environ.get("GOOGLE_SPREADSHEET_ID", "").strip()
118
+ if not spreadsheet_id:
119
+ raise GoogleOAuthError("GOOGLE_SPREADSHEET_ID nao configurado.")
120
+
121
+ service = get_sheets_service()
122
+ fields = [
123
+ "timestamp",
124
+ "query",
125
+ "product_id",
126
+ "product_name",
127
+ "categoria_inferida",
128
+ "feedback",
129
+ "motivo",
130
+ "score_final",
131
+ "score_semantico",
132
+ "bonus_lexical",
133
+ "penalidade_feedback",
134
+ "user_message",
135
+ ]
136
+ row = [[str(feedback_data.get(field, "") or "") for field in fields]]
137
+
138
+ return service.spreadsheets().values().append(
139
+ spreadsheetId=spreadsheet_id,
140
+ range="Feedback!A:Z",
141
+ valueInputOption="USER_ENTERED",
142
+ insertDataOption="INSERT_ROWS",
143
+ body={"values": row},
144
+ ).execute()
app/main.py CHANGED
@@ -1,186 +1,238 @@
1
- import os
2
- import threading
3
- from pathlib import Path
4
-
5
- from fastapi import FastAPI, Response
6
- from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.responses import FileResponse, RedirectResponse
8
- from pydantic import BaseModel
9
- from typing import Optional
10
-
11
- from .agent import ShoppingAgent
12
- from .feedback import caminho_feedback, salvar_feedback
13
- from .logger import salvar_log_busca
14
- from .memory import caminho_memoria_negativa
15
-
16
- EMBEDDING_PROVIDER = os.getenv("EMBEDDING_PROVIDER", "transformers").strip().lower()
17
- HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "Ana2012/bertimbau-buscador").strip()
18
-
19
-
20
-
21
- def _env_flag(name, default="true"):
22
- return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"}
23
-
24
-
25
- PRELOAD_AGENT = _env_flag("PRELOAD_AGENT", "true")
26
- LOGS_DIR = os.getenv("LOGS_DIR", "/data/logs")
27
- DATA_DIR = "/data"
28
-
29
- app = FastAPI(title="TCC2 Agent API")
30
-
31
- app.add_middleware(
32
- CORSMiddleware,
33
- # Libera temporariamente a comunicacao entre frontend na Cloudflare e backend no Fly.io.
34
- allow_origins=["*"],
35
- allow_credentials=False,
36
- allow_methods=["*"],
37
- allow_headers=["*"],
38
- )
39
-
40
- agent = None
41
- agent_lock = threading.Lock()
42
-
43
-
44
- def get_agent():
45
- global agent
46
- if agent is None:
47
- with agent_lock:
48
- if agent is None:
49
- agent = ShoppingAgent()
50
- return agent
51
-
52
-
53
- @app.on_event("startup")
54
- def preload_agent():
55
- if PRELOAD_AGENT:
56
- get_agent()
57
-
58
-
59
- class ChatRequest(BaseModel):
60
- query: Optional[str] = None
61
- message: Optional[str] = None
62
- top_k: int = 5
63
-
64
-
65
- class FeedbackRequest(BaseModel):
66
- query: str
67
- product_id: str
68
- product_name: str
69
- rating: Optional[int] = None
70
- is_helpful: Optional[bool] = None
71
-
72
-
73
- @app.get("/health")
74
- def health():
75
- runtime = get_agent().runtime_info() if agent is not None else None
76
- return {
77
- "status": "ok",
78
- "agent_ready": agent is not None,
79
- "embedding_provider": EMBEDDING_PROVIDER,
80
- "model_repo": HF_MODEL_REPO,
81
- "preload_agent": PRELOAD_AGENT,
82
- "runtime": runtime,
83
- }
84
-
85
-
86
- @app.get("/", include_in_schema=False)
87
- def root():
88
- return RedirectResponse(url="/docs")
89
-
90
-
91
- @app.get("/favicon.ico", include_in_schema=False)
92
- def favicon():
93
- return Response(status_code=204)
94
-
95
-
96
- @app.get("/debug/files")
97
- def debug_files():
98
- data_path = Path(DATA_DIR)
99
- logs_path = Path(LOGS_DIR)
100
- feedback_path = Path(caminho_feedback())
101
- memory_path = Path(caminho_memoria_negativa())
102
-
103
- return {
104
- "data_exists": data_path.exists(),
105
- "logs_exists": logs_path.exists(),
106
- "feedback_exists": feedback_path.exists(),
107
- "negative_memory_exists": memory_path.exists(),
108
- "data_files": sorted(p.name for p in data_path.iterdir()) if data_path.exists() else [],
109
- "logs_files": sorted(p.name for p in logs_path.iterdir()) if logs_path.exists() else [],
110
- "feedback_file": str(feedback_path),
111
- "negative_memory_file": str(memory_path),
112
- }
113
-
114
-
115
- @app.get("/debug/feedback")
116
- def debug_feedback():
117
- feedback_path = Path(caminho_feedback())
118
- if not feedback_path.exists():
119
- return {"error": "arquivo nao existe"}
120
-
121
- return {"conteudo": feedback_path.read_text(encoding="utf-8")}
122
-
123
-
124
- @app.get("/download/feedback")
125
- def download_feedback():
126
- feedback_path = caminho_feedback()
127
- if not os.path.exists(feedback_path):
128
- return {"error": "arquivo nao existe"}
129
-
130
- return FileResponse(feedback_path, filename="feedback.csv")
131
-
132
-
133
- @app.get("/debug/memory")
134
- def debug_memory():
135
- memory_path = Path(caminho_memoria_negativa())
136
- if not memory_path.exists():
137
- return {"status": "missing", "file": str(memory_path)}
138
-
139
- return {
140
- "status": "ok",
141
- "file": str(memory_path),
142
- "content": memory_path.read_text(encoding="utf-8"),
143
- }
144
-
145
-
146
- @app.post("/chat")
147
- def chat(request: ChatRequest):
148
- texto = request.query or request.message
149
-
150
- if not texto:
151
- return {"error": "query ou message deve ser informado"}
152
-
153
- resultado = get_agent().responder(texto, top_k=request.top_k)
154
- salvar_log_busca(resultado)
155
- return resultado
156
-
157
-
158
- @app.post("/feedback")
159
- def feedback(request: FeedbackRequest):
160
- feedback_file = caminho_feedback()
161
- print(
162
- "Salvando feedback:",
163
- {
164
- "query": request.query,
165
- "product_id": request.product_id,
166
- "feedback_file": feedback_file,
167
- "logs_dir_exists": os.path.exists(LOGS_DIR),
168
- },
169
- )
170
-
171
- try:
172
- return salvar_feedback(
173
- query=request.query,
174
- product_id=request.product_id,
175
- product_name=request.product_name,
176
- rating=request.rating,
177
- is_helpful=request.is_helpful
178
- )
179
- except Exception as exc:
180
- return {
181
- "status": "error",
182
- "message": "Erro ao salvar feedback.",
183
- "detail": str(exc),
184
- "feedback_file": feedback_file,
185
- "logs_dir_exists": os.path.exists(LOGS_DIR),
186
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import threading
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from fastapi import FastAPI, Query, Response
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
9
+ from pydantic import BaseModel
10
+
11
+ from .agent import ShoppingAgent
12
+ from .feedback import caminho_feedback, google_sheets_habilitado, salvar_feedback
13
+ from .google_oauth import build_flow, get_authorization_url, load_credentials, save_credentials
14
+ from .logger import salvar_log_busca
15
+ from .memory import caminho_memoria_negativa
16
+
17
+ EMBEDDING_PROVIDER = os.getenv("EMBEDDING_PROVIDER", "transformers").strip().lower()
18
+ HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "Ana2012/bertimbau-buscador").strip()
19
+
20
+
21
+ def _env_flag(name, default="true"):
22
+ return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"}
23
+
24
+
25
+ PRELOAD_AGENT = _env_flag("PRELOAD_AGENT", "true")
26
+ LOGS_DIR = os.getenv("LOGS_DIR", "/data/logs")
27
+ DATA_DIR = "/data"
28
+
29
+ app = FastAPI(title="TCC2 Agent API")
30
+
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_credentials=False,
35
+ allow_methods=["*"],
36
+ allow_headers=["*"],
37
+ )
38
+
39
+ agent = None
40
+ agent_lock = threading.Lock()
41
+
42
+
43
+ def get_agent():
44
+ global agent
45
+ if agent is None:
46
+ with agent_lock:
47
+ if agent is None:
48
+ agent = ShoppingAgent()
49
+ return agent
50
+
51
+
52
+ @app.on_event("startup")
53
+ def preload_agent():
54
+ if PRELOAD_AGENT:
55
+ get_agent()
56
+
57
+
58
+ class ChatRequest(BaseModel):
59
+ query: Optional[str] = None
60
+ message: Optional[str] = None
61
+ top_k: int = 5
62
+
63
+
64
+ class FeedbackRequest(BaseModel):
65
+ query: str
66
+ product_id: str
67
+ product_name: str
68
+ rating: Optional[int] = None
69
+ is_helpful: Optional[bool] = None
70
+ note: Optional[str] = None
71
+ categoria_inferida: Optional[str] = None
72
+ feedback: Optional[str] = None
73
+ motivo: Optional[str] = None
74
+ score_final: Optional[float] = None
75
+ score_semantico: Optional[float] = None
76
+ bonus_lexical: Optional[float] = None
77
+ penalidade_feedback: Optional[float] = None
78
+ user_message: Optional[str] = None
79
+
80
+
81
+ @app.get("/health")
82
+ def health():
83
+ runtime = get_agent().runtime_info() if agent is not None else None
84
+ return {
85
+ "status": "ok",
86
+ "agent_ready": agent is not None,
87
+ "embedding_provider": EMBEDDING_PROVIDER,
88
+ "model_repo": HF_MODEL_REPO,
89
+ "preload_agent": PRELOAD_AGENT,
90
+ "runtime": runtime,
91
+ "feedback_storage": "google_sheets" if google_sheets_habilitado() else "csv",
92
+ }
93
+
94
+
95
+ @app.get("/", include_in_schema=False)
96
+ def root():
97
+ return RedirectResponse(url="/docs")
98
+
99
+
100
+ @app.get("/favicon.ico", include_in_schema=False)
101
+ def favicon():
102
+ return Response(status_code=204)
103
+
104
+
105
+ @app.get("/auth/google")
106
+ def auth_google():
107
+ authorization_url, _state = get_authorization_url()
108
+ return RedirectResponse(url=authorization_url)
109
+
110
+
111
+ @app.get("/oauth2callback")
112
+ def oauth2callback(code: str = Query(...)):
113
+ flow = build_flow()
114
+ flow.fetch_token(code=code)
115
+ save_credentials(flow.credentials)
116
+ return HTMLResponse(
117
+ "<h3>Autorizacao concluida. O backend ja pode salvar feedbacks no Google Sheets.</h3>"
118
+ )
119
+
120
+
121
+ @app.get("/auth/status")
122
+ def auth_status():
123
+ credentials = load_credentials()
124
+ connected = credentials is not None and credentials.valid
125
+ return {
126
+ "google_sheets_connected": connected,
127
+ "message": (
128
+ "Google Sheets autorizado e pronto para uso."
129
+ if connected
130
+ else "Google Sheets ainda nao autorizado. Acesse /auth/google para conectar."
131
+ ),
132
+ }
133
+
134
+
135
+ @app.get("/debug/files")
136
+ def debug_files():
137
+ data_path = Path(DATA_DIR)
138
+ logs_path = Path(LOGS_DIR)
139
+ feedback_path = Path(caminho_feedback())
140
+ memory_path = Path(caminho_memoria_negativa())
141
+
142
+ return {
143
+ "data_exists": data_path.exists(),
144
+ "logs_exists": logs_path.exists(),
145
+ "feedback_exists": feedback_path.exists(),
146
+ "negative_memory_exists": memory_path.exists(),
147
+ "data_files": sorted(p.name for p in data_path.iterdir()) if data_path.exists() else [],
148
+ "logs_files": sorted(p.name for p in logs_path.iterdir()) if logs_path.exists() else [],
149
+ "feedback_file": str(feedback_path),
150
+ "negative_memory_file": str(memory_path),
151
+ "feedback_storage": "google_sheets" if google_sheets_habilitado() else "csv",
152
+ }
153
+
154
+
155
+ @app.get("/debug/feedback")
156
+ def debug_feedback():
157
+ feedback_path = Path(caminho_feedback())
158
+ if not feedback_path.exists():
159
+ return {"error": "arquivo nao existe"}
160
+
161
+ return {"conteudo": feedback_path.read_text(encoding="utf-8")}
162
+
163
+
164
+ @app.get("/download/feedback")
165
+ def download_feedback():
166
+ feedback_path = caminho_feedback()
167
+ if not os.path.exists(feedback_path):
168
+ return {"error": "arquivo nao existe"}
169
+
170
+ return FileResponse(feedback_path, filename="feedback.csv")
171
+
172
+
173
+ @app.get("/debug/memory")
174
+ def debug_memory():
175
+ memory_path = Path(caminho_memoria_negativa())
176
+ if not memory_path.exists():
177
+ return {"status": "missing", "file": str(memory_path)}
178
+
179
+ return {
180
+ "status": "ok",
181
+ "file": str(memory_path),
182
+ "content": memory_path.read_text(encoding="utf-8"),
183
+ }
184
+
185
+
186
+ @app.post("/chat")
187
+ def chat(request: ChatRequest):
188
+ texto = request.query or request.message
189
+
190
+ if not texto:
191
+ return {"error": "query ou message deve ser informado"}
192
+
193
+ resultado = get_agent().responder(texto, top_k=request.top_k)
194
+ salvar_log_busca(resultado)
195
+ return resultado
196
+
197
+
198
+ @app.post("/feedback")
199
+ def feedback(request: FeedbackRequest):
200
+ feedback_file = caminho_feedback()
201
+ print(
202
+ "Salvando feedback:",
203
+ {
204
+ "query": request.query,
205
+ "product_id": request.product_id,
206
+ "feedback_file": feedback_file,
207
+ "logs_dir_exists": os.path.exists(LOGS_DIR),
208
+ "google_sheets_enabled": google_sheets_habilitado(),
209
+ },
210
+ )
211
+
212
+ try:
213
+ return salvar_feedback(
214
+ query=request.query,
215
+ product_id=request.product_id,
216
+ product_name=request.product_name,
217
+ rating=request.rating,
218
+ is_helpful=request.is_helpful,
219
+ note=request.note,
220
+ categoria_inferida=request.categoria_inferida,
221
+ feedback=request.feedback,
222
+ motivo=request.motivo,
223
+ score_final=request.score_final,
224
+ score_semantico=request.score_semantico,
225
+ bonus_lexical=request.bonus_lexical,
226
+ penalidade_feedback=request.penalidade_feedback,
227
+ user_message=request.user_message,
228
+ )
229
+ except Exception as exc:
230
+ return {
231
+ "ok": False,
232
+ "saved_local": False,
233
+ "saved_google_sheets": False,
234
+ "detail": str(exc),
235
+ "feedback_file": feedback_file,
236
+ "logs_dir_exists": os.path.exists(LOGS_DIR),
237
+ "google_sheets_enabled": google_sheets_habilitado(),
238
+ }