antonypamo commited on
Commit
bb31657
·
verified ·
1 Parent(s): 3756a01

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +235 -80
app.py CHANGED
@@ -1,44 +1,51 @@
1
  import os
 
 
 
2
  import numpy as np
3
  from numpy.linalg import norm
4
  from scipy.linalg import expm
 
 
 
 
5
  from sentence_transformers import SentenceTransformer
6
  from huggingface_hub import hf_hub_download
7
  import joblib
8
 
9
- from fastapi import FastAPI
10
- from pydantic import BaseModel, Field
11
- from typing import Optional, Dict, Any, List
12
 
13
- # NOTE: HF_TOKEN is expected to be set as an environment variable in a real deployment
14
- # For local testing, you might set it here or pass it directly
15
- HF_TOKEN = os.environ.get("HF_TOKEN", "") # Use environment variable, default to empty
16
  os.environ["HF_TOKEN"] = HF_TOKEN
17
 
18
- ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE" # encoder RRF
19
- META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogit" # repo del meta-logit
20
- META_LOGIT_FILENAME = "logreg_rrf_savant.joblib" # NUEVO archivo del meta-logit en HF
21
 
22
- print("🔄 Cargando encoder RRFSAVANTMADE...")
23
  encoder = SentenceTransformer(ENCODER_MODEL_ID)
 
24
 
25
- print("🔄 Descargando meta-logit v2 desde HF Hub...")
26
  meta_logit_path = hf_hub_download(
27
  repo_id=META_LOGIT_REPO,
28
  filename=META_LOGIT_FILENAME,
29
- token=os.environ.get("HF_TOKEN")
30
  )
31
-
32
- print("🔄 Cargando modelo meta-logit v2...")
33
  meta_logit = joblib.load(meta_logit_path)
34
-
35
- print(" Encoder y meta-logit v2 cargados correctamente.")
 
 
 
36
 
37
 
38
- # =========================
39
- # Geometría icosaédrica
40
- # (Copied from cell lyVrwdhgIOlq)
41
- # =========================
42
 
43
  phi = (1 + np.sqrt(5)) / 2
44
  nodes = np.array([
@@ -49,19 +56,21 @@ nodes = np.array([
49
  nodes /= norm(nodes, axis=1, keepdims=True)
50
  N = nodes.shape[0] # 12 nodos
51
 
52
- # Pauli
53
  sigma_x = np.array([[0, 1], [1, 0]], dtype=complex)
54
  sigma_y = np.array([[0, -1j], [1j, 0]], dtype=complex)
55
  sigma_z = np.array([[1, 0], [0, -1]], dtype=complex)
56
 
 
57
  def kron_IN(M, N_sites):
58
  return np.kron(M, np.eye(N_sites, dtype=complex))
59
 
 
60
  def site_op(block_2x2, i, j, N_sites):
61
  K = np.zeros((N_sites, N_sites), dtype=complex)
62
  K[i, j] = 1.0
63
  return np.kron(K, block_2x2)
64
 
 
65
  def geodesic_kernel(nodes, sigma=0.618, alpha_log=0.10):
66
  diff = nodes[:, None, :] - nodes[None, :, :]
67
  dist = norm(diff, axis=-1)
@@ -78,6 +87,7 @@ def geodesic_kernel(nodes, sigma=0.618, alpha_log=0.10):
78
  row_sums[row_sums == 0] = 1.0
79
  return W / row_sums
80
 
 
81
  def u1_edge_phases(nodes, flux_vector=(0.0, 0.0, 0.0), q=1.0, gauge_scale=1.0):
82
  A = gauge_scale * np.asarray(flux_vector, dtype=float)
83
  midpoints = (nodes[:, None, :] + nodes[None, :, :]) / 2.0
@@ -85,6 +95,7 @@ def u1_edge_phases(nodes, flux_vector=(0.0, 0.0, 0.0), q=1.0, gauge_scale=1.0):
85
  theta = 0.5 * (theta - theta.T)
86
  return theta * q
87
 
 
88
  def build_dirac_hamiltonian(
89
  m=0.25,
90
  v=1.0,
@@ -92,7 +103,7 @@ def build_dirac_hamiltonian(
92
  alpha_log=0.10,
93
  q=1.0,
94
  flux_vector=(0.0, 0.0, 0.0),
95
- gauge_scale=0.0
96
  ):
97
  W = geodesic_kernel(nodes, sigma=sigma, alpha_log=alpha_log)
98
 
@@ -103,10 +114,8 @@ def build_dirac_hamiltonian(
103
  else:
104
  U = np.ones((N, N), dtype=complex)
105
 
106
- # Término de masa
107
  H = np.kron(np.eye(N, dtype=complex), m * sigma_z)
108
 
109
- # Término cinético acoplado
110
  diff = nodes[:, None, :] - nodes[None, :, :]
111
  dist = norm(diff, axis=-1) + 1e-12
112
  d_hat = diff / dist[..., None]
@@ -121,27 +130,31 @@ def build_dirac_hamiltonian(
121
  nvec[2] * sigma_z)
122
  H += v * W[i, j] * U[i, j] * site_op(S, i, j, N)
123
 
124
- # Hermitizar por seguridad numérica
125
  H = 0.5 * (H + H.conj().T)
126
  return H
127
 
 
128
  def site_probs(psi):
129
  N2 = psi.shape[0]
130
  n = N2 // 2
131
  psi_mat = psi.reshape(n, 2)
132
  return np.sum(np.abs(psi_mat)**2, axis=1).real
133
 
 
134
  def chirality(psi):
135
  S = kron_IN(sigma_z, N)
136
  return float(np.vdot(psi, S @ psi).real)
137
 
 
138
  def energy_expectation(psi, H):
139
  return float(np.vdot(psi, H @ psi).real)
140
 
 
141
  def spatial_entropy(p):
142
  p = np.clip(p, 1e-12, 1.0)
143
  return float(-np.sum(p * np.log(p)).real)
144
 
 
145
  def evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20):
146
  U = expm(-1j * dt * H)
147
  psi = psi0.copy()
@@ -172,30 +185,28 @@ def evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20):
172
  }
173
 
174
 
175
- # =========================
176
- # Feature extraction and scoring
177
- # (Copied from cell DiknqWJZIZ5q)
178
- # =========================
179
 
180
  def get_embedding(text: str) -> np.ndarray:
181
  emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)
182
  return emb[0]
183
 
184
- def compute_rrf_features(prompt: str, answer: str) -> dict:
185
- # Embeddings RRF
 
186
  e_p = get_embedding(prompt)
187
  e_a = get_embedding(answer)
188
 
189
  cosine_pa = float(np.dot(e_p, e_a))
190
  len_ratio = len(answer) / (len(prompt) + 1.0)
191
 
192
- # Estado inicial ligado al texto (seed reproducible)
193
  rng = np.random.default_rng(abs(hash(prompt + answer)) % (2**32))
194
  vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
195
  vec /= np.sqrt(np.vdot(vec, vec))
196
  psi0 = vec
197
 
198
- # Hamiltoniano Dirac Φ12.0
199
  H = build_dirac_hamiltonian(
200
  m=0.25, v=1.0, sigma=0.618,
201
  alpha_log=0.10, q=1.0,
@@ -203,21 +214,20 @@ def compute_rrf_features(prompt: str, answer: str) -> dict:
203
  gauge_scale=0.0
204
  )
205
 
206
- out = evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20)
207
 
208
- probs = out["probs"]
209
  energy = out["energy"]
210
  chir = out["chirality"]
211
- entropy = out["entropy"]
212
 
213
- S_initial = float(entropy[0])
214
  S_final = float(entropy[-1])
 
215
  S_delta = S_final - S_initial
216
  C_final = float(chir[-1])
217
  E_mean = float(np.mean(energy))
218
  E_std = float(np.std(energy))
219
 
220
- return {
221
  "cosine_pa": cosine_pa,
222
  "len_ratio": len_ratio,
223
  "dirac_entropy_final": S_final,
@@ -227,7 +237,21 @@ def compute_rrf_features(prompt: str, answer: str) -> dict:
227
  "dirac_energy_std": E_std,
228
  }
229
 
230
- def features_to_vector(feats: dict) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  keys = [
232
  "cosine_pa",
233
  "len_ratio",
@@ -236,24 +260,30 @@ def features_to_vector(feats: dict) -> np.ndarray:
236
  "dirac_chirality_final",
237
  "dirac_energy_mean",
238
  "dirac_energy_std",
 
 
 
 
 
 
 
 
239
  ]
240
  return np.array([feats[k] for k in keys], dtype=float)
241
 
 
242
  def compute_scores_srff_crrf_ephi(prompt: str, answer: str):
243
  feats = compute_rrf_features(prompt, answer)
244
  x = features_to_vector(feats).reshape(1, -1)
245
 
246
- # meta-logit v2: pipeline (scaler + logistic regression)
247
  proba = meta_logit.predict_proba(x)[0]
248
  p_good = float(proba[1])
249
 
250
  SRRF = p_good
251
  CRRF = p_good * feats["cosine_pa"]
252
 
253
- S_final = feats["dirac_entropy_final"]
254
- S_max = np.log(N)
255
- norm_entropy = float(S_final / S_max)
256
-
257
  E_phi = 0.5 * (SRRF + norm_entropy)
258
 
259
  scores = {
@@ -265,16 +295,9 @@ def compute_scores_srff_crrf_ephi(prompt: str, answer: str):
265
  return scores, feats
266
 
267
 
268
- # =========================
269
- # FastAPI App
270
- # (Copied from cell LwlyX4-LIgKK)
271
- # =========================
272
-
273
- app = FastAPI(
274
- title="Savant RRF Φ12.0 API",
275
- description="Evaluación conceptual resonante para texto generado por LLMs (SRRF / CRRF / E_phi).",
276
- version="1.0.0",
277
- )
278
 
279
  class EvaluateRequest(BaseModel):
280
  prompt: str = Field(..., description="Pregunta / instrucción original.")
@@ -283,41 +306,173 @@ class EvaluateRequest(BaseModel):
283
  None, description="Etiqueta opcional del modelo que generó la respuesta."
284
  )
285
 
 
286
  class EvaluateResponse(BaseModel):
287
  scores: Dict[str, float]
288
  features: Dict[str, float]
289
  sim_summary: Dict[str, Any]
290
 
291
- @app.post("/evaluate", response_model=EvaluateResponse)
292
- def evaluate_endpoint(req: EvaluateRequest):
293
- scores, feats = compute_scores_srff_crrf_ephi(req.prompt, req.answer)
294
 
295
- # mini-sim extra para resumen diagnóstico simple
296
- H = build_dirac_hamiltonian(
297
- m=0.25, v=1.0, sigma=0.618,
298
- alpha_log=0.10, q=1.0,
299
- flux_vector=(0.0, 0.0, 0.0),
300
- gauge_scale=0.0
 
 
 
 
 
 
 
 
301
  )
302
- rng = np.random.default_rng(abs(hash(req.prompt + req.answer)) % (2**32))
303
- vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
304
- vec /= np.sqrt(np.vdot(vec, vec))
305
- psi0 = vec
 
 
 
 
 
 
 
 
306
 
307
- sim = evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25)
308
 
309
- sim_summary = {
310
- "entropy_initial": float(sim["entropy"][0]),
311
- "entropy_final": float(sim["entropy"][-1]),
312
- "chirality_initial": float(sim["chirality"][0]),
313
- "chirality_final": float(sim["chirality"][-1]),
314
- "energy_mean": float(np.mean(sim["energy"])),
315
- "energy_std": float(np.std(sim["energy"])),
316
- "N_sites": int(N),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
 
 
 
 
 
 
 
318
 
319
- return EvaluateResponse(
320
- scores=scores,
321
- features=feats,
322
- sim_summary=sim_summary,
 
323
  )
 
1
  import os
2
+ import math
3
+ from typing import Optional, Dict, Any, List
4
+
5
  import numpy as np
6
  from numpy.linalg import norm
7
  from scipy.linalg import expm
8
+
9
+ from fastapi import FastAPI, HTTPException
10
+ from pydantic import BaseModel, Field
11
+
12
  from sentence_transformers import SentenceTransformer
13
  from huggingface_hub import hf_hub_download
14
  import joblib
15
 
16
+ # ============================
17
+ # Configuración de modelos
18
+ # ============================
19
 
20
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
 
21
  os.environ["HF_TOKEN"] = HF_TOKEN
22
 
23
+ ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
24
+ META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogit"
25
+ META_LOGIT_FILENAME = "logreg_rrf_savant_15.joblib" # versión 15 features
26
 
27
+ print("🔄 [Startup] Cargando encoder RRFSAVANTMADE...", flush=True)
28
  encoder = SentenceTransformer(ENCODER_MODEL_ID)
29
+ print("✅ [Startup] Encoder cargado.", flush=True)
30
 
31
+ print("🔄 [Startup] Descargando meta-logit desde HF Hub...", flush=True)
32
  meta_logit_path = hf_hub_download(
33
  repo_id=META_LOGIT_REPO,
34
  filename=META_LOGIT_FILENAME,
35
+ token=HF_TOKEN if HF_TOKEN else None,
36
  )
37
+ print(f"🔄 [Startup] Cargando modelo meta-logit '{META_LOGIT_FILENAME}'...", flush=True)
 
38
  meta_logit = joblib.load(meta_logit_path)
39
+ try:
40
+ print(f"🔎 [Startup] Meta-logit espera {meta_logit.n_features_in_} features.", flush=True)
41
+ except Exception:
42
+ print("⚠️ [Startup] No se pudo leer n_features_in_.", flush=True)
43
+ print("✅ [Startup] Meta-logit cargado.", flush=True)
44
 
45
 
46
+ # ============================
47
+ # Geometría icosaédrica Φ12.0
48
+ # ============================
 
49
 
50
  phi = (1 + np.sqrt(5)) / 2
51
  nodes = np.array([
 
56
  nodes /= norm(nodes, axis=1, keepdims=True)
57
  N = nodes.shape[0] # 12 nodos
58
 
 
59
  sigma_x = np.array([[0, 1], [1, 0]], dtype=complex)
60
  sigma_y = np.array([[0, -1j], [1j, 0]], dtype=complex)
61
  sigma_z = np.array([[1, 0], [0, -1]], dtype=complex)
62
 
63
+
64
  def kron_IN(M, N_sites):
65
  return np.kron(M, np.eye(N_sites, dtype=complex))
66
 
67
+
68
  def site_op(block_2x2, i, j, N_sites):
69
  K = np.zeros((N_sites, N_sites), dtype=complex)
70
  K[i, j] = 1.0
71
  return np.kron(K, block_2x2)
72
 
73
+
74
  def geodesic_kernel(nodes, sigma=0.618, alpha_log=0.10):
75
  diff = nodes[:, None, :] - nodes[None, :, :]
76
  dist = norm(diff, axis=-1)
 
87
  row_sums[row_sums == 0] = 1.0
88
  return W / row_sums
89
 
90
+
91
  def u1_edge_phases(nodes, flux_vector=(0.0, 0.0, 0.0), q=1.0, gauge_scale=1.0):
92
  A = gauge_scale * np.asarray(flux_vector, dtype=float)
93
  midpoints = (nodes[:, None, :] + nodes[None, :, :]) / 2.0
 
95
  theta = 0.5 * (theta - theta.T)
96
  return theta * q
97
 
98
+
99
  def build_dirac_hamiltonian(
100
  m=0.25,
101
  v=1.0,
 
103
  alpha_log=0.10,
104
  q=1.0,
105
  flux_vector=(0.0, 0.0, 0.0),
106
+ gauge_scale=0.0,
107
  ):
108
  W = geodesic_kernel(nodes, sigma=sigma, alpha_log=alpha_log)
109
 
 
114
  else:
115
  U = np.ones((N, N), dtype=complex)
116
 
 
117
  H = np.kron(np.eye(N, dtype=complex), m * sigma_z)
118
 
 
119
  diff = nodes[:, None, :] - nodes[None, :, :]
120
  dist = norm(diff, axis=-1) + 1e-12
121
  d_hat = diff / dist[..., None]
 
130
  nvec[2] * sigma_z)
131
  H += v * W[i, j] * U[i, j] * site_op(S, i, j, N)
132
 
 
133
  H = 0.5 * (H + H.conj().T)
134
  return H
135
 
136
+
137
  def site_probs(psi):
138
  N2 = psi.shape[0]
139
  n = N2 // 2
140
  psi_mat = psi.reshape(n, 2)
141
  return np.sum(np.abs(psi_mat)**2, axis=1).real
142
 
143
+
144
  def chirality(psi):
145
  S = kron_IN(sigma_z, N)
146
  return float(np.vdot(psi, S @ psi).real)
147
 
148
+
149
  def energy_expectation(psi, H):
150
  return float(np.vdot(psi, H @ psi).real)
151
 
152
+
153
  def spatial_entropy(p):
154
  p = np.clip(p, 1e-12, 1.0)
155
  return float(-np.sum(p * np.log(p)).real)
156
 
157
+
158
  def evolve_dirac_shell(psi0, H, dt=0.05, steps=200, record_every=20):
159
  U = expm(-1j * dt * H)
160
  psi = psi0.copy()
 
185
  }
186
 
187
 
188
+ # ============================
189
+ # Core RRF: embeddings + features + scores
190
+ # ============================
 
191
 
192
  def get_embedding(text: str) -> np.ndarray:
193
  emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)
194
  return emb[0]
195
 
196
+
197
+ def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
198
+ # Embeddings
199
  e_p = get_embedding(prompt)
200
  e_a = get_embedding(answer)
201
 
202
  cosine_pa = float(np.dot(e_p, e_a))
203
  len_ratio = len(answer) / (len(prompt) + 1.0)
204
 
 
205
  rng = np.random.default_rng(abs(hash(prompt + answer)) % (2**32))
206
  vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
207
  vec /= np.sqrt(np.vdot(vec, vec))
208
  psi0 = vec
209
 
 
210
  H = build_dirac_hamiltonian(
211
  m=0.25, v=1.0, sigma=0.618,
212
  alpha_log=0.10, q=1.0,
 
214
  gauge_scale=0.0
215
  )
216
 
217
+ out = evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25)
218
 
219
+ entropy = out["entropy"]
220
  energy = out["energy"]
221
  chir = out["chirality"]
 
222
 
 
223
  S_final = float(entropy[-1])
224
+ S_initial = float(entropy[0])
225
  S_delta = S_final - S_initial
226
  C_final = float(chir[-1])
227
  E_mean = float(np.mean(energy))
228
  E_std = float(np.std(energy))
229
 
230
+ feats: Dict[str, float] = {
231
  "cosine_pa": cosine_pa,
232
  "len_ratio": len_ratio,
233
  "dirac_entropy_final": S_final,
 
237
  "dirac_energy_std": E_std,
238
  }
239
 
240
+ # Derivadas extra para llegar a 15 features
241
+ S_max = math.log(N)
242
+ feats["entropy_norm"] = feats["dirac_entropy_final"] / S_max
243
+ feats["entropy_abs_delta"] = abs(feats["dirac_entropy_delta"])
244
+ feats["chirality_abs"] = abs(feats["dirac_chirality_final"])
245
+ feats["energy_abs_mean"] = abs(feats["dirac_energy_mean"])
246
+ feats["energy_std_sq"] = feats["dirac_energy_std"] ** 2
247
+ feats["cosine_sq"] = feats["cosine_pa"] ** 2
248
+ feats["len_log"] = math.log1p(feats["len_ratio"])
249
+ feats["len_inv"] = 1.0 / (1.0 + feats["len_ratio"])
250
+
251
+ return feats
252
+
253
+
254
+ def features_to_vector(feats: Dict[str, float]) -> np.ndarray:
255
  keys = [
256
  "cosine_pa",
257
  "len_ratio",
 
260
  "dirac_chirality_final",
261
  "dirac_energy_mean",
262
  "dirac_energy_std",
263
+ "entropy_norm",
264
+ "entropy_abs_delta",
265
+ "chirality_abs",
266
+ "energy_abs_mean",
267
+ "energy_std_sq",
268
+ "cosine_sq",
269
+ "len_log",
270
+ "len_inv",
271
  ]
272
  return np.array([feats[k] for k in keys], dtype=float)
273
 
274
+
275
  def compute_scores_srff_crrf_ephi(prompt: str, answer: str):
276
  feats = compute_rrf_features(prompt, answer)
277
  x = features_to_vector(feats).reshape(1, -1)
278
 
 
279
  proba = meta_logit.predict_proba(x)[0]
280
  p_good = float(proba[1])
281
 
282
  SRRF = p_good
283
  CRRF = p_good * feats["cosine_pa"]
284
 
285
+ S_max = math.log(N)
286
+ norm_entropy = float(feats["dirac_entropy_final"] / S_max)
 
 
287
  E_phi = 0.5 * (SRRF + norm_entropy)
288
 
289
  scores = {
 
295
  return scores, feats
296
 
297
 
298
+ # ============================
299
+ # FastAPI app
300
+ # ============================
 
 
 
 
 
 
 
301
 
302
  class EvaluateRequest(BaseModel):
303
  prompt: str = Field(..., description="Pregunta / instrucción original.")
 
306
  None, description="Etiqueta opcional del modelo que generó la respuesta."
307
  )
308
 
309
+
310
  class EvaluateResponse(BaseModel):
311
  scores: Dict[str, float]
312
  features: Dict[str, float]
313
  sim_summary: Dict[str, Any]
314
 
 
 
 
315
 
316
+ class QualityRemoteRequest(EvaluateRequest):
317
+ """Alias de EvaluateRequest para /quality_remote."""
318
+ pass
319
+
320
+
321
+ class RerankRequest(BaseModel):
322
+ """
323
+ Petición para /v1/rerank
324
+ """
325
+ query: str = Field(..., description="Query de búsqueda o pregunta del usuario.")
326
+ documents: List[str] = Field(..., description="Lista de documentos candidatos a rerankear.")
327
+ alpha: float = Field(
328
+ 0.2,
329
+ description="Peso de la corrección log_rdf en el score_final. 0 = solo cosine, 1 = solo log_rdf."
330
  )
331
+ query_embedding_norm: bool = Field(
332
+ True,
333
+ description="Si True, normaliza el embedding de query (útil para cosine)."
334
+ )
335
+
336
+
337
+ class RerankDocumentResult(BaseModel):
338
+ id: int = Field(..., description="Índice del documento en la lista de entrada.")
339
+ score_cosine: float
340
+ score_log_rdf: float
341
+ score_final: float
342
+ rank: int
343
 
 
344
 
345
+ class RerankResponse(BaseModel):
346
+ model_id: str
347
+ alpha: float
348
+ query_embedding_norm: bool
349
+ results: List[RerankDocumentResult]
350
+
351
+
352
+ app = FastAPI(
353
+ title="Savant RRF Φ12.0 API",
354
+ description="Dirac-Resonant conceptual quality + reranking para texto generado por LLMs.",
355
+ version="1.0.0",
356
+ )
357
+
358
+
359
+ @app.get("/")
360
+ def root():
361
+ return {"message": "Savant RRF Φ12.0 API running", "docs": "/docs"}
362
+
363
+
364
+ @app.get("/health")
365
+ def health():
366
+ return {
367
+ "status": "ok",
368
+ "encoder_model_id": ENCODER_MODEL_ID,
369
+ "meta_logit_filename": META_LOGIT_FILENAME,
370
+ "N_sites": N,
371
+ }
372
+
373
+
374
+ @app.post("/evaluate", response_model=EvaluateResponse)
375
+ def evaluate_endpoint(req: EvaluateRequest):
376
+ try:
377
+ scores, feats = compute_scores_srff_crrf_ephi(req.prompt, req.answer)
378
+
379
+ H = build_dirac_hamiltonian(
380
+ m=0.25, v=1.0, sigma=0.618,
381
+ alpha_log=0.10, q=1.0,
382
+ flux_vector=(0.0, 0.0, 0.0),
383
+ gauge_scale=0.0
384
+ )
385
+ rng = np.random.default_rng(abs(hash(req.prompt + req.answer + "sim")) % (2**32))
386
+ vec = rng.normal(0, 1, (2*N,)) + 1j * rng.normal(0, 1, (2*N,))
387
+ vec /= np.sqrt(np.vdot(vec, vec))
388
+ psi0 = vec
389
+ sim = evolve_dirac_shell(psi0, H, dt=0.05, steps=60, record_every=20)
390
+
391
+ sim_summary = {
392
+ "entropy_initial": float(sim["entropy"][0]),
393
+ "entropy_final": float(sim["entropy"][-1]),
394
+ "chirality_initial": float(sim["chirality"][0]),
395
+ "chirality_final": float(sim["chirality"][-1]),
396
+ "energy_mean": float(np.mean(sim["energy"])),
397
+ "energy_std": float(np.std(sim["energy"])),
398
+ "N_sites": int(N),
399
+ }
400
+
401
+ return EvaluateResponse(
402
+ scores=scores,
403
+ features=feats,
404
+ sim_summary=sim_summary,
405
+ )
406
+ except Exception as e:
407
+ print(f"❌ [Runtime] Error en /evaluate: {e}", flush=True)
408
+ raise HTTPException(status_code=500, detail="Internal server error")
409
+
410
+
411
+ @app.post("/quality_remote", response_model=EvaluateResponse)
412
+ def quality_remote(req: QualityRemoteRequest):
413
+ """Alias remoto de /evaluate."""
414
+ return evaluate_endpoint(req)
415
+
416
+
417
+ def _compute_rerank_scores(query: str, docs: List[str], alpha: float, norm_query: bool) -> List[RerankDocumentResult]:
418
+ q_emb = encoder.encode([query], convert_to_numpy=True, normalize_embeddings=norm_query)[0]
419
+
420
+ results = []
421
+ for idx, text in enumerate(docs):
422
+ d_emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)[0]
423
+ score_cosine = float(np.dot(q_emb, d_emb))
424
+
425
+ val = max(score_cosine, 0.0) + 1e-6
426
+ score_log_rdf = float(np.log1p(val))
427
+
428
+ score_final = (1.0 - alpha) * score_cosine + alpha * score_log_rdf
429
+
430
+ results.append(
431
+ {
432
+ "id": idx,
433
+ "score_cosine": score_cosine,
434
+ "score_log_rdf": score_log_rdf,
435
+ "score_final": score_final,
436
+ }
437
+ )
438
+
439
+ results_sorted = sorted(results, key=lambda r: r["score_final"], reverse=True)
440
+ reranked = []
441
+ for rank, r in enumerate(results_sorted, start=1):
442
+ reranked.append(
443
+ RerankDocumentResult(
444
+ id=r["id"],
445
+ score_cosine=r["score_cosine"],
446
+ score_log_rdf=r["score_log_rdf"],
447
+ score_final=r["score_final"],
448
+ rank=rank,
449
+ )
450
+ )
451
+ return reranked
452
+
453
+
454
+ @app.post("/v1/rerank", response_model=RerankResponse)
455
+ def rerank_endpoint(req: RerankRequest):
456
+ """
457
+ Endpoint Savant Seek:
458
+ POST /v1/rerank
459
+ {
460
+ "query": "...",
461
+ "documents": ["doc1", "doc2", ...],
462
+ "alpha": 0.2,
463
+ "query_embedding_norm": true
464
  }
465
+ """
466
+ results = _compute_rerank_scores(
467
+ query=req.query,
468
+ docs=req.documents,
469
+ alpha=req.alpha,
470
+ norm_query=req.query_embedding_norm,
471
+ )
472
 
473
+ return RerankResponse(
474
+ model_id=ENCODER_MODEL_ID,
475
+ alpha=req.alpha,
476
+ query_embedding_norm=req.query_embedding_norm,
477
+ results=results,
478
  )