antonypamo commited on
Commit
60b9ffe
·
verified ·
1 Parent(s): 026bb37

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +303 -138
main.py CHANGED
@@ -1,55 +1,114 @@
1
  import os
2
- import sys
3
- import math
4
- from typing import Optional, Dict, Any
5
 
6
  import numpy as np
7
  from numpy.linalg import norm
8
  from scipy.linalg import expm
9
-
10
- from fastapi import FastAPI, HTTPException
11
- from pydantic import BaseModel
12
-
13
  from sentence_transformers import SentenceTransformer
14
  from huggingface_hub import hf_hub_download
15
  import joblib
16
 
17
- # ============================
18
- # Configuración de modelos
19
- # ============================
20
- ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
21
- META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogit"
22
- META_LOGIT_FILENAME = "logreg_rrf_savant_15.joblib"
23
-
24
- print("🔄 [Startup] Cargando encoder RRFSAVANTMADE...", flush=True)
25
- try:
26
- encoder = SentenceTransformer(ENCODER_MODEL_ID)
27
- print("✅ [Startup] Encoder cargado.", flush=True)
28
- except Exception as e:
29
- print(f"❌ [Startup] Error al cargar encoder: {e}", file=sys.stderr, flush=True)
30
- raise
31
-
32
- print("🔄 [Startup] Descargando meta-logit desde HF Hub...", flush=True)
33
- try:
34
- meta_logit_path = hf_hub_download(
35
- repo_id=META_LOGIT_REPO,
36
- filename=META_LOGIT_FILENAME,
37
- token=os.environ.get("HF_TOKEN"), # si el repo es público, puede ser None
38
- )
39
- print(f"🔄 [Startup] Cargando modelo meta-logit '{META_LOGIT_FILENAME}'...", flush=True)
40
- meta_logit = joblib.load(meta_logit_path)
41
- try:
42
- print(f"🔎 [Startup] Meta-logit espera {meta_logit.n_features_in_} features.", flush=True)
43
- except Exception:
44
- print("⚠️ [Startup] No se pudo leer n_features_in_.", flush=True)
45
- print("✅ [Startup] Meta-logit cargado.", flush=True)
46
- except Exception as e:
47
- print(f"❌ [Startup] Error al cargar meta-logit: {e}", file=sys.stderr, flush=True)
48
- raise
49
-
50
- # ============================
51
- # Geometría icosaédrica Φ12.0
52
- # ============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  phi = (1 + np.sqrt(5)) / 2
55
  nodes = np.array([
@@ -79,11 +138,11 @@ def geodesic_kernel(nodes, sigma=0.618, alpha_log=0.10):
79
  diff = nodes[:, None, :] - nodes[None, :, :]
80
  dist = norm(diff, axis=-1)
81
 
82
- W = np.exp(-(dist ** 2) / (sigma ** 2))
83
  np.fill_diagonal(W, 0.0)
84
 
85
  if alpha_log > 0.0:
86
- corr = 1.0 + alpha_log * np.log1p(dist ** 2)
87
  corr[range(N), range(N)] = 1.0
88
  W = W / corr
89
 
@@ -112,8 +171,7 @@ def build_dirac_hamiltonian(
112
  W = geodesic_kernel(nodes, sigma=sigma, alpha_log=alpha_log)
113
 
114
  if gauge_scale != 0.0 and any(flux_vector):
115
- theta = u1_edge_phases(nodes, flux_vector=flux_vector,
116
- q=q, gauge_scale=gauge_scale)
117
  U = np.exp(1j * theta)
118
  else:
119
  U = np.ones((N, N), dtype=complex)
@@ -142,7 +200,7 @@ def site_probs(psi):
142
  N2 = psi.shape[0]
143
  n = N2 // 2
144
  psi_mat = psi.reshape(n, 2)
145
- return np.sum(np.abs(psi_mat) ** 2, axis=1).real
146
 
147
 
148
  def chirality(psi):
@@ -159,7 +217,7 @@ def spatial_entropy(p):
159
  return float(-np.sum(p * np.log(p)).real)
160
 
161
 
162
- def evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25):
163
  U = expm(-1j * dt * H)
164
  psi = psi0.copy()
165
 
@@ -188,9 +246,10 @@ def evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25):
188
  "record_every": record_every,
189
  }
190
 
191
- # ============================
192
- # Core RRF: embeddings + features + scores
193
- # ============================
 
194
 
195
  def get_embedding(text: str) -> np.ndarray:
196
  emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)
@@ -198,16 +257,14 @@ def get_embedding(text: str) -> np.ndarray:
198
 
199
 
200
  def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
201
- # Embeddings
202
  e_p = get_embedding(prompt)
203
  e_a = get_embedding(answer)
204
 
205
  cosine_pa = float(np.dot(e_p, e_a))
206
  len_ratio = len(answer) / (len(prompt) + 1.0)
207
 
208
- # Simulación Dirac shell determinista (semilla por prompt+answer)
209
- rng = np.random.default_rng(abs(hash(prompt + answer)) % (2 ** 32))
210
- vec = rng.normal(0, 1, (2 * N,)) + 1j * rng.normal(0, 1, (2 * N,))
211
  vec /= np.sqrt(np.vdot(vec, vec))
212
  psi0 = vec
213
 
@@ -215,24 +272,23 @@ def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
215
  m=0.25, v=1.0, sigma=0.618,
216
  alpha_log=0.10, q=1.0,
217
  flux_vector=(0.0, 0.0, 0.0),
218
- gauge_scale=0.0,
219
  )
220
 
221
- out = evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25)
222
 
223
- entropy = out["entropy"]
224
  energy = out["energy"]
225
  chir = out["chirality"]
 
226
 
227
- S_final = float(entropy[-1])
228
  S_initial = float(entropy[0])
 
229
  S_delta = S_final - S_initial
230
  C_final = float(chir[-1])
231
  E_mean = float(np.mean(energy))
232
  E_std = float(np.std(energy))
233
 
234
- # Núcleo de 7 features
235
- feats: Dict[str, float] = {
236
  "cosine_pa": cosine_pa,
237
  "len_ratio": len_ratio,
238
  "dirac_entropy_final": S_final,
@@ -242,22 +298,12 @@ def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
242
  "dirac_energy_std": E_std,
243
  }
244
 
245
- # Derivadas para llegar a 15 (igual que en el CSV)
246
- S_max = math.log(N)
247
- feats["entropy_norm"] = feats["dirac_entropy_final"] / S_max
248
- feats["entropy_abs_delta"] = abs(feats["dirac_entropy_delta"])
249
- feats["chirality_abs"] = abs(feats["dirac_chirality_final"])
250
- feats["energy_abs_mean"] = abs(feats["dirac_energy_mean"])
251
- feats["energy_std_sq"] = feats["dirac_energy_std"] ** 2
252
- feats["cosine_sq"] = feats["cosine_pa"] ** 2
253
- feats["len_log"] = math.log1p(feats["len_ratio"])
254
- feats["len_inv"] = 1.0 / (1.0 + feats["len_ratio"])
255
-
256
- return feats
257
 
258
-
259
- def features_to_vector(feats: Dict[str, float]) -> np.ndarray:
260
- keys = [
 
 
261
  "cosine_pa",
262
  "len_ratio",
263
  "dirac_entropy_final",
@@ -265,31 +311,43 @@ def features_to_vector(feats: Dict[str, float]) -> np.ndarray:
265
  "dirac_chirality_final",
266
  "dirac_energy_mean",
267
  "dirac_energy_std",
268
- "entropy_norm",
269
- "entropy_abs_delta",
270
- "chirality_abs",
271
- "energy_abs_mean",
272
- "energy_std_sq",
273
- "cosine_sq",
274
- "len_log",
275
- "len_inv",
276
  ]
277
- return np.array([feats[k] for k in keys], dtype=float)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
 
279
 
280
- def compute_scores_srff_crff_ephi(prompt: str, answer: str):
 
281
  feats = compute_rrf_features(prompt, answer)
282
- x = features_to_vector(feats).reshape(1, -1)
283
 
284
  proba = meta_logit.predict_proba(x)[0]
285
  p_good = float(proba[1])
286
 
287
- # Definimos SRRF/CRRF/E_phi a partir de p_good y entropía
288
  SRRF = p_good
289
  CRRF = p_good * feats["cosine_pa"]
290
 
291
- S_max = math.log(N)
292
- norm_entropy = float(feats["dirac_entropy_final"] / S_max)
 
293
  E_phi = 0.5 * (SRRF + norm_entropy)
294
 
295
  scores = {
@@ -300,67 +358,174 @@ def compute_scores_srff_crff_ephi(prompt: str, answer: str):
300
  }
301
  return scores, feats
302
 
303
- # ============================
304
- # FastAPI app
305
- # ============================
 
 
 
 
 
 
 
 
 
 
306
 
307
  class EvaluateRequest(BaseModel):
308
  prompt: str
309
  answer: str
310
  model_label: Optional[str] = None
311
 
 
312
  class EvaluateResponse(BaseModel):
313
  scores: Dict[str, float]
314
  features: Dict[str, float]
315
  sim_summary: Dict[str, Any]
316
 
317
- app = FastAPI(
318
- title="Savant RRF Φ12.0 API",
319
- description="Dirac-Resonant conceptual quality layer for LLM-generated text.",
320
- version="1.0.0",
321
- )
322
 
323
- @app.get("/")
324
- def root():
325
- return {"message": "Savant RRF Φ12.0 API running", "docs": "/docs"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- @app.get("/health")
328
- def health():
329
- return {"status": "ok"}
330
 
331
- @app.post("/evaluate", response_model=EvaluateResponse)
332
- def evaluate(req: EvaluateRequest):
 
 
 
 
 
 
333
  try:
334
- scores, feats = compute_scores_srff_crff_ephi(req.prompt, req.answer)
335
-
336
- # resumen de una simulación adicional (fresca) solo para info
337
- H = build_dirac_hamiltonian(
338
- m=0.25, v=1.0, sigma=0.618,
339
- alpha_log=0.10, q=1.0,
340
- flux_vector=(0.0, 0.0, 0.0),
341
- gauge_scale=0.0,
342
  )
343
- rng = np.random.default_rng(abs(hash(req.prompt + req.answer + "sim")) % (2 ** 32))
344
- vec = rng.normal(0, 1, (2 * N,)) + 1j * rng.normal(0, 1, (2 * N,))
345
- vec /= np.sqrt(np.vdot(vec, vec))
346
- psi0 = vec
347
- sim = evolve_dirac_shell(psi0, H, dt=0.05, steps=60, record_every=20)
348
-
349
- sim_summary = {
350
- "entropy_initial": float(sim["entropy"][0]),
351
- "entropy_final": float(sim["entropy"][-1]),
352
- "chirality_initial": float(sim["chirality"][0]),
353
- "chirality_final": float(sim["chirality"][-1]),
354
- "energy_mean": float(np.mean(sim["energy"])),
355
- "energy_std": float(np.std(sim["energy"])),
356
- "N_sites": int(N),
357
- }
358
-
359
- return EvaluateResponse(
360
- scores=scores,
361
- features=feats,
362
- sim_summary=sim_summary,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  )
364
- except Exception as e:
365
- print(f"❌ [Runtime] Error en /evaluate: {e}", file=sys.stderr, flush=True)
366
- raise HTTPException(status_code=500, detail="Internal server error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import time
3
+ import logging
4
+ from typing import Optional, Dict, Any, List
5
 
6
  import numpy as np
7
  from numpy.linalg import norm
8
  from scipy.linalg import expm
 
 
 
 
9
  from sentence_transformers import SentenceTransformer
10
  from huggingface_hub import hf_hub_download
11
  import joblib
12
 
13
+ from fastapi import FastAPI, Depends, Header, HTTPException, status, Request
14
+ from fastapi.responses import JSONResponse
15
+ from pydantic import BaseModel
16
+
17
+
18
+ # ============================================================
19
+ # 0. LOGGING BÁSICO
20
+ # ============================================================
21
+
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format="%(asctime)s [%(levelname)s] %(message)s",
25
+ )
26
+ logger = logging.getLogger("savant-api")
27
+
28
+
29
+ # ============================================================
30
+ # 1. CONFIGURACIÓN Y API KEYS
31
+ # ============================================================
32
+
33
+ # HF token
34
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
35
+ os.environ["HF_TOKEN"] = HF_TOKEN
36
+
37
+ # API keys (muy simple para MVP)
38
+ # - SAVANT_API_KEY: una sola API key
39
+ # - SAVANT_API_KEYS: lista separada por comas ("key1,key2,...")
40
+ single_key = os.environ.get("SAVANT_API_KEY", "").strip()
41
+ multi_keys = os.environ.get("SAVANT_API_KEYS", "")
42
+ allowed_keys = set(k.strip() for k in multi_keys.split(",") if k.strip())
43
+ if single_key:
44
+ allowed_keys.add(single_key)
45
+
46
+ if not allowed_keys:
47
+ logger.warning("⚠️ No hay API keys configuradas. La API aceptará TODO tráfico (MODO ABIERTO).")
48
+ else:
49
+ logger.info(f"🔐 API Keys configuradas: {len(allowed_keys)}")
50
+
51
+
52
+ def api_key_dependency(
53
+ x_api_key: Optional[str] = Header(default=None, alias="x-api-key"),
54
+ authorization: Optional[str] = Header(default=None),
55
+ ):
56
+ """
57
+ Dependencia FastAPI para proteger endpoints con API key.
58
+ Acepta:
59
+ - Header: x-api-key: <KEY>
60
+ - Header: Authorization: Bearer <KEY>
61
+ """
62
+ if not allowed_keys:
63
+ # Modo abierto: no validamos nada (útil para testing / dev).
64
+ return
65
+
66
+ candidate = None
67
+ if x_api_key:
68
+ candidate = x_api_key.strip()
69
+ elif authorization and authorization.lower().startswith("bearer "):
70
+ candidate = authorization.split(" ", 1)[1].strip()
71
+
72
+ if not candidate or candidate not in allowed_keys:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_401_UNAUTHORIZED,
75
+ detail="Invalid or missing API key",
76
+ )
77
+
78
+
79
+ # ============================================================
80
+ # 2. CARGA DE MODELOS (ENCODER + META-LOGIT)
81
+ # ============================================================
82
+
83
+ ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
84
+ META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogit"
85
+ META_LOGIT_FILENAME = "logreg_rrf_savant_15.joblib" # versión 15-features
86
+
87
+ logger.info("===== Application Startup =====")
88
+ logger.info("🔄 [Startup] Cargando encoder RRFSAVANTMADE...")
89
+
90
+ encoder = SentenceTransformer(ENCODER_MODEL_ID)
91
+
92
+ logger.info("✅ [Startup] Encoder cargado.")
93
+ logger.info("🔄 [Startup] Descargando meta-logit desde HF Hub...")
94
+
95
+ meta_logit_path = hf_hub_download(
96
+ repo_id=META_LOGIT_REPO,
97
+ filename=META_LOGIT_FILENAME,
98
+ token=HF_TOKEN if HF_TOKEN else None,
99
+ )
100
+
101
+ logger.info(f"🔄 [Startup] Cargando modelo meta-logit '{META_LOGIT_FILENAME}'...")
102
+ meta_logit = joblib.load(meta_logit_path)
103
+
104
+ n_features_expected = getattr(meta_logit, "n_features_in_", None)
105
+ logger.info(f"🔎 [Startup] Meta-logit espera {n_features_expected} features.")
106
+ logger.info("✅ [Startup] Meta-logit cargado.")
107
+
108
+
109
+ # ============================================================
110
+ # 3. GEOMETRÍA ICOSAÉDRICA RRF
111
+ # ============================================================
112
 
113
  phi = (1 + np.sqrt(5)) / 2
114
  nodes = np.array([
 
138
  diff = nodes[:, None, :] - nodes[None, :, :]
139
  dist = norm(diff, axis=-1)
140
 
141
+ W = np.exp(-(dist**2) / (sigma**2))
142
  np.fill_diagonal(W, 0.0)
143
 
144
  if alpha_log > 0.0:
145
+ corr = 1.0 + alpha_log * np.log1p(dist**2)
146
  corr[range(N), range(N)] = 1.0
147
  W = W / corr
148
 
 
171
  W = geodesic_kernel(nodes, sigma=sigma, alpha_log=alpha_log)
172
 
173
  if gauge_scale != 0.0 and any(flux_vector):
174
+ theta = u1_edge_phases(nodes, flux_vector=flux_vector, q=q, gauge_scale=gauge_scale)
 
175
  U = np.exp(1j * theta)
176
  else:
177
  U = np.ones((N, N), dtype=complex)
 
200
  N2 = psi.shape[0]
201
  n = N2 // 2
202
  psi_mat = psi.reshape(n, 2)
203
+ return np.sum(np.abs(psi_mat)**2, axis=1).real
204
 
205
 
206
  def chirality(psi):
 
217
  return float(-np.sum(p * np.log(p)).real)
218
 
219
 
220
+ def evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20):
221
  U = expm(-1j * dt * H)
222
  psi = psi0.copy()
223
 
 
246
  "record_every": record_every,
247
  }
248
 
249
+
250
+ # ============================================================
251
+ # 4. FEATURES RRF + META-LOGIT (QUALITY)
252
+ # ============================================================
253
 
254
  def get_embedding(text: str) -> np.ndarray:
255
  emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)
 
257
 
258
 
259
  def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
 
260
  e_p = get_embedding(prompt)
261
  e_a = get_embedding(answer)
262
 
263
  cosine_pa = float(np.dot(e_p, e_a))
264
  len_ratio = len(answer) / (len(prompt) + 1.0)
265
 
266
+ rng = np.random.default_rng(abs(hash(prompt + answer)) % (2**32))
267
+ vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
 
268
  vec /= np.sqrt(np.vdot(vec, vec))
269
  psi0 = vec
270
 
 
272
  m=0.25, v=1.0, sigma=0.618,
273
  alpha_log=0.10, q=1.0,
274
  flux_vector=(0.0, 0.0, 0.0),
275
+ gauge_scale=0.0
276
  )
277
 
278
+ out = evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20)
279
 
 
280
  energy = out["energy"]
281
  chir = out["chirality"]
282
+ entropy = out["entropy"]
283
 
 
284
  S_initial = float(entropy[0])
285
+ S_final = float(entropy[-1])
286
  S_delta = S_final - S_initial
287
  C_final = float(chir[-1])
288
  E_mean = float(np.mean(energy))
289
  E_std = float(np.std(energy))
290
 
291
+ return {
 
292
  "cosine_pa": cosine_pa,
293
  "len_ratio": len_ratio,
294
  "dirac_entropy_final": S_final,
 
298
  "dirac_energy_std": E_std,
299
  }
300
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ def features_to_vector(feats: dict, meta_logit_model) -> np.ndarray:
303
+ """
304
+ Adapta las features RRF al nº de features que espera el meta-logit.
305
+ """
306
+ base_keys = [
307
  "cosine_pa",
308
  "len_ratio",
309
  "dirac_entropy_final",
 
311
  "dirac_chirality_final",
312
  "dirac_energy_mean",
313
  "dirac_energy_std",
 
 
 
 
 
 
 
 
314
  ]
315
+ x_base = np.array([feats[k] for k in base_keys], dtype=float)
316
+
317
+ n_expected = getattr(meta_logit_model, "n_features_in_", x_base.shape[0])
318
+
319
+ if n_expected == x_base.shape[0]:
320
+ return x_base
321
+
322
+ x_full = np.zeros((n_expected,), dtype=float)
323
+
324
+ if hasattr(meta_logit_model, "feature_names_in_"):
325
+ feature_names = list(meta_logit_model.feature_names_in_)
326
+ for i, name in enumerate(feature_names):
327
+ if name in feats:
328
+ x_full[i] = float(feats[name])
329
+ else:
330
+ x_full[i] = 0.0
331
+ else:
332
+ n_copy = min(n_expected, x_base.shape[0])
333
+ x_full[:n_copy] = x_base[:n_copy]
334
 
335
+ return x_full
336
 
337
+
338
+ def compute_scores_srff_crrf_ephi(prompt: str, answer: str):
339
  feats = compute_rrf_features(prompt, answer)
340
+ x = features_to_vector(feats, meta_logit).reshape(1, -1)
341
 
342
  proba = meta_logit.predict_proba(x)[0]
343
  p_good = float(proba[1])
344
 
 
345
  SRRF = p_good
346
  CRRF = p_good * feats["cosine_pa"]
347
 
348
+ S_final = feats["dirac_entropy_final"]
349
+ S_max = np.log(N)
350
+ norm_entropy = float(S_final / S_max)
351
  E_phi = 0.5 * (SRRF + norm_entropy)
352
 
353
  scores = {
 
358
  }
359
  return scores, feats
360
 
361
+
362
+ # ============================================================
363
+ # 5. FASTAPI APP
364
+ # ============================================================
365
+
366
+ app = FastAPI(
367
+ title="Savant RRF Φ12.0 API",
368
+ description="Savant RRF Quality (/v1/quality) y Savant RRF Seek (/v1/rerank)",
369
+ version="1.0.0",
370
+ )
371
+
372
+
373
+ # ----------------- MODELOS Pydantic -----------------
374
 
375
  class EvaluateRequest(BaseModel):
376
  prompt: str
377
  answer: str
378
  model_label: Optional[str] = None
379
 
380
+
381
  class EvaluateResponse(BaseModel):
382
  scores: Dict[str, float]
383
  features: Dict[str, float]
384
  sim_summary: Dict[str, Any]
385
 
 
 
 
 
 
386
 
387
+ class RerankRequest(BaseModel):
388
+ query: str
389
+ documents: List[str]
390
+ alpha: float = 0.2
391
+ query_embedding_norm: bool = True
392
+
393
+
394
+ class RerankDocumentResult(BaseModel):
395
+ id: int
396
+ score_cosine: float
397
+ score_log_rdf: float
398
+ score_final: float
399
+ rank: int
400
+
401
+
402
+ class RerankResponse(BaseModel):
403
+ model_id: str
404
+ alpha: float
405
+ query_embedding_norm: bool
406
+ results: List[RerankDocumentResult]
407
 
 
 
 
408
 
409
+ # ============================================================
410
+ # 6. ENDPOINTS
411
+ # ============================================================
412
+
413
+ @app.middleware("http")
414
+ async def log_requests(request: Request, call_next):
415
+ start_time = time.time()
416
+ response = None
417
  try:
418
+ response = await call_next(request)
419
+ return response
420
+ finally:
421
+ process_time = (time.time() - start_time) * 1000
422
+ logger.info(
423
+ f"[Request] {request.method} {request.url.path} "
424
+ f"status={response.status_code if response else 'ERR'} "
425
+ f"time_ms={process_time:.2f}"
426
  )
427
+
428
+
429
+ @app.get("/health")
430
+ def health_check():
431
+ return {
432
+ "status": "ok",
433
+ "encoder_model_id": ENCODER_MODEL_ID,
434
+ "meta_logit_filename": META_LOGIT_FILENAME,
435
+ "meta_logit_n_features": n_features_expected,
436
+ "N_sites": N,
437
+ }
438
+
439
+
440
+ @app.post("/evaluate", response_model=EvaluateResponse, dependencies=[Depends(api_key_dependency)])
441
+ def evaluate_endpoint(req: EvaluateRequest):
442
+ scores, feats = compute_scores_srff_crrf_ephi(req.prompt, req.answer)
443
+
444
+ H = build_dirac_hamiltonian(
445
+ m=0.25, v=1.0, sigma=0.618,
446
+ alpha_log=0.10, q=1.0,
447
+ flux_vector=(0.0, 0.0, 0.0),
448
+ gauge_scale=0.0
449
+ )
450
+ rng = np.random.default_rng(abs(hash(req.prompt + req.answer)) % (2**32))
451
+ vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
452
+ vec /= np.sqrt(np.vdot(vec, vec))
453
+ psi0 = vec
454
+
455
+ sim = evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25)
456
+
457
+ sim_summary = {
458
+ "entropy_initial": float(sim["entropy"][0]),
459
+ "entropy_final": float(sim["entropy"][-1]),
460
+ "chirality_initial": float(sim["chirality"][0]),
461
+ "chirality_final": float(sim["chirality"][-1]),
462
+ "energy_mean": float(np.mean(sim["energy"])),
463
+ "energy_std": float(np.std(sim["energy"])),
464
+ "N_sites": int(N),
465
+ }
466
+
467
+ return EvaluateResponse(
468
+ scores=scores,
469
+ features=feats,
470
+ sim_summary=sim_summary,
471
+ )
472
+
473
+
474
+ @app.post("/v1/quality", response_model=EvaluateResponse, dependencies=[Depends(api_key_dependency)])
475
+ def quality_v1_endpoint(req: EvaluateRequest):
476
+ # Alias directo de /evaluate
477
+ return evaluate_endpoint(req)
478
+
479
+
480
+ def _compute_rerank_scores(query: str, docs: List[str], alpha: float, norm_query: bool) -> List[RerankDocumentResult]:
481
+ q_emb = encoder.encode([query], convert_to_numpy=True, normalize_embeddings=norm_query)[0]
482
+
483
+ results = []
484
+ for idx, text in enumerate(docs):
485
+ d_emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)[0]
486
+ score_cosine = float(np.dot(q_emb, d_emb))
487
+
488
+ val = max(score_cosine, 0.0) + 1e-6
489
+ score_log_rdf = float(np.log1p(val))
490
+
491
+ score_final = (1.0 - alpha) * score_cosine + alpha * score_log_rdf
492
+
493
+ results.append(
494
+ {
495
+ "id": idx,
496
+ "score_cosine": score_cosine,
497
+ "score_log_rdf": score_log_rdf,
498
+ "score_final": score_final,
499
+ }
500
+ )
501
+
502
+ results_sorted = sorted(results, key=lambda r: r["score_final"], reverse=True)
503
+ reranked = []
504
+ for rank, r in enumerate(results_sorted, start=1):
505
+ reranked.append(
506
+ RerankDocumentResult(
507
+ id=r["id"],
508
+ score_cosine=r["score_cosine"],
509
+ score_log_rdf=r["score_log_rdf"],
510
+ score_final=r["score_final"],
511
+ rank=rank,
512
+ )
513
  )
514
+ return reranked
515
+
516
+
517
+ @app.post("/v1/rerank", response_model=RerankResponse, dependencies=[Depends(api_key_dependency)])
518
+ def rerank_endpoint(req: RerankRequest):
519
+ results = _compute_rerank_scores(
520
+ query=req.query,
521
+ docs=req.documents,
522
+ alpha=req.alpha,
523
+ norm_query=req.query_embedding_norm,
524
+ )
525
+
526
+ return RerankResponse(
527
+ model_id=ENCODER_MODEL_ID,
528
+ alpha=req.alpha,
529
+ query_embedding_norm=req.query_embedding_norm,
530
+ results=results,
531
+ )