antonypamo commited on
Commit
7ba7d54
·
verified ·
1 Parent(s): b5bb4b5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +176 -615
main.py CHANGED
@@ -1,711 +1,272 @@
1
- import os
2
- import sys
3
- import math
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
-
10
  from fastapi import FastAPI, HTTPException
11
  from pydantic import BaseModel, Field
12
 
13
  from sentence_transformers import SentenceTransformer
14
  from huggingface_hub import hf_hub_download
15
- from datasets import load_dataset
16
  import joblib
17
 
18
  # ============================
19
- # Configuración de modelos
20
  # ============================
21
 
22
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
23
 
24
- ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
25
- META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogit"
26
- META_LOGIT_FILENAME = "logreg_rrf_savant_15.joblib"
27
- RRF_TUTOR_DATASET_ID = "antonypamo/savant_rrf1"
28
-
29
- print("🔄 [Startup] Cargando encoder RRFSAVANTMADE...", flush=True)
30
- try:
31
- encoder = SentenceTransformer(ENCODER_MODEL_ID)
32
- print("✅ [Startup] Encoder cargado.", flush=True)
33
- except Exception as e:
34
- print(f"❌ [Startup] Error al cargar encoder: {e}", file=sys.stderr, flush=True)
35
- raise
36
-
37
- print("🔄 [Startup] Descargando meta-logit desde HF Hub...", flush=True)
38
- try:
39
- meta_logit_path = hf_hub_download(
40
- repo_id=META_LOGIT_REPO,
41
- filename=META_LOGIT_FILENAME,
42
- token=HF_TOKEN if HF_TOKEN else None,
43
- )
44
- print(f"🔄 [Startup] Cargando modelo meta-logit '{META_LOGIT_FILENAME}'...", flush=True)
45
- meta_logit = joblib.load(meta_logit_path)
46
- try:
47
- print(f"🔎 [Startup] Meta-logit espera {meta_logit.n_features_in_} features.", flush=True)
48
- except Exception:
49
- print("⚠️ [Startup] No se pudo leer n_features_in_.", flush=True)
50
- print("✅ [Startup] Meta-logit cargado.", flush=True)
51
- except Exception as e:
52
- print(f"❌ [Startup] Error al cargar meta-logit: {e}", file=sys.stderr, flush=True)
53
- raise
54
-
55
 
56
  # ============================
57
- # Geometría icosaédrica Φ12.0
58
  # ============================
59
 
60
- phi = (1 + np.sqrt(5)) / 2
61
- nodes = np.array([
62
- [0, 1, phi], [0, -1, phi], [0, 1, -phi], [0, -1, -phi],
63
- [1, phi, 0], [-1, phi, 0], [1, -phi, 0], [-1, -phi, 0],
64
- [phi, 0, 1], [phi, 0, -1], [-phi, 0, 1], [-phi, 0, -1]
65
- ], dtype=float)
66
- nodes /= norm(nodes, axis=1, keepdims=True)
67
- N = nodes.shape[0] # 12 nodos
68
-
69
- sigma_x = np.array([[0, 1], [1, 0]], dtype=complex)
70
- sigma_y = np.array([[0, -1j], [1j, 0]], dtype=complex)
71
- sigma_z = np.array([[1, 0], [0, -1]], dtype=complex)
72
-
73
-
74
- def kron_IN(M, N_sites):
75
- return np.kron(M, np.eye(N_sites, dtype=complex))
76
-
77
-
78
- def site_op(block_2x2, i, j, N_sites):
79
- K = np.zeros((N_sites, N_sites), dtype=complex)
80
- K[i, j] = 1.0
81
- return np.kron(K, block_2x2)
82
-
83
-
84
- def geodesic_kernel(nodes, sigma=0.618, alpha_log=0.10):
85
- diff = nodes[:, None, :] - nodes[None, :, :]
86
- dist = norm(diff, axis=-1)
87
-
88
- W = np.exp(-(dist ** 2) / (sigma ** 2))
89
- np.fill_diagonal(W, 0.0)
90
-
91
- if alpha_log > 0.0:
92
- corr = 1.0 + alpha_log * np.log1p(dist ** 2)
93
- corr[range(N), range(N)] = 1.0
94
- W = W / corr
95
-
96
- row_sums = W.sum(axis=1, keepdims=True)
97
- row_sums[row_sums == 0] = 1.0
98
- return W / row_sums
99
-
100
-
101
- def u1_edge_phases(nodes, flux_vector=(0.0, 0.0, 0.0), q=1.0, gauge_scale=1.0):
102
- A = gauge_scale * np.asarray(flux_vector, dtype=float)
103
- midpoints = (nodes[:, None, :] + nodes[None, :, :]) / 2.0
104
- theta = (midpoints @ A).astype(float)
105
- theta = 0.5 * (theta - theta.T)
106
- return theta * q
107
-
108
-
109
- def build_dirac_hamiltonian(
110
- m=0.25,
111
- v=1.0,
112
- sigma=0.618,
113
- alpha_log=0.10,
114
- q=1.0,
115
- flux_vector=(0.0, 0.0, 0.0),
116
- gauge_scale=0.0,
117
- ):
118
- W = geodesic_kernel(nodes, sigma=sigma, alpha_log=alpha_log)
119
-
120
- if gauge_scale != 0.0 and any(flux_vector):
121
- theta = u1_edge_phases(nodes, flux_vector=flux_vector,
122
- q=q, gauge_scale=gauge_scale)
123
- U = np.exp(1j * theta)
124
- else:
125
- U = np.ones((N, N), dtype=complex)
126
-
127
- H = np.kron(np.eye(N, dtype=complex), m * sigma_z)
128
 
129
- diff = nodes[:, None, :] - nodes[None, :, :]
130
- dist = norm(diff, axis=-1) + 1e-12
131
- d_hat = diff / dist[..., None]
132
-
133
- for i in range(N):
134
- for j in range(N):
135
- if i == j or W[i, j] == 0:
136
- continue
137
- nvec = d_hat[i, j]
138
- S = (nvec[0] * sigma_x +
139
- nvec[1] * sigma_y +
140
- nvec[2] * sigma_z)
141
- H += v * W[i, j] * U[i, j] * site_op(S, i, j, N)
142
-
143
- H = 0.5 * (H + H.conj().T)
144
- return H
145
-
146
-
147
- def site_probs(psi):
148
- N2 = psi.shape[0]
149
- n = N2 // 2
150
- psi_mat = psi.reshape(n, 2)
151
- return np.sum(np.abs(psi_mat) ** 2, axis=1).real
152
-
153
-
154
- def chirality(psi):
155
- S = kron_IN(sigma_z, N)
156
- return float(np.vdot(psi, S @ psi).real)
157
-
158
-
159
- def energy_expectation(psi, H):
160
- return float(np.vdot(psi, H @ psi).real)
161
-
162
-
163
- def spatial_entropy(p):
164
- p = np.clip(p, 1e-12, 1.0)
165
- return float(-np.sum(p * np.log(p)).real)
166
-
167
-
168
- def evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25):
169
- U = expm(-1j * dt * H)
170
- psi = psi0.copy()
171
-
172
- probs_hist = []
173
- energy_hist = []
174
- chir_hist = []
175
- ent_hist = []
176
-
177
- for t in range(steps + 1):
178
- if t % record_every == 0:
179
- p = site_probs(psi)
180
- probs_hist.append(p)
181
- energy_hist.append(energy_expectation(psi, H))
182
- chir_hist.append(chirality(psi))
183
- ent_hist.append(spatial_entropy(p))
184
-
185
- psi = U @ psi
186
- psi /= np.sqrt(np.vdot(psi, psi))
187
 
188
- return {
189
- "probs": np.array(probs_hist, dtype=float),
190
- "energy": np.array(energy_hist, dtype=float),
191
- "chirality": np.array(chir_hist, dtype=float),
192
- "entropy": np.array(ent_hist, dtype=float),
193
- "dt": dt,
194
- "record_every": record_every,
195
- }
196
 
 
197
 
198
  # ============================
199
- # Core RRF: embeddings + features + scores
200
  # ============================
201
 
202
  def get_embedding(text: str) -> np.ndarray:
203
- emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)
204
- return emb[0]
205
-
206
-
207
- def compute_rrf_features(prompt: str, answer: str) -> Dict[str, float]:
208
- # Embeddings
209
- e_p = get_embedding(prompt)
210
- e_a = get_embedding(answer)
211
-
212
- cosine_pa = float(np.dot(e_p, e_a))
213
- len_ratio = len(answer) / (len(prompt) + 1.0)
214
-
215
- # Simulación Dirac shell determinista (semilla por prompt+answer)
216
- rng = np.random.default_rng(abs(hash(prompt + answer)) % (2 ** 32))
217
- vec = rng.normal(0, 1, (2 * N,)) + 1j * rng.normal(0, 1, (2 * N,))
218
- vec /= np.sqrt(np.vdot(vec, vec))
219
- psi0 = vec
220
-
221
- H = build_dirac_hamiltonian(
222
- m=0.25, v=1.0, sigma=0.618,
223
- alpha_log=0.10, q=1.0,
224
- flux_vector=(0.0, 0.0, 0.0),
225
- gauge_scale=0.0,
226
- )
227
 
228
- out = evolve_dirac_shell(psi0, H, dt=0.05, steps=100, record_every=25)
229
-
230
- entropy = out["entropy"]
231
- energy = out["energy"]
232
- chir = out["chirality"]
233
-
234
- S_final = float(entropy[-1])
235
- S_initial = float(entropy[0])
236
- S_delta = S_final - S_initial
237
- C_final = float(chir[-1])
238
- E_mean = float(np.mean(energy))
239
- E_std = float(np.std(energy))
240
-
241
- feats: Dict[str, float] = {
242
- "cosine_pa": cosine_pa,
243
- "len_ratio": len_ratio,
244
- "dirac_entropy_final": S_final,
245
- "dirac_entropy_delta": S_delta,
246
- "dirac_chirality_final": C_final,
247
- "dirac_energy_mean": E_mean,
248
- "dirac_energy_std": E_std,
249
- }
250
 
251
- # Derivadas para llegar a 15 (igual que en tu CSV/meta-logit)
252
- S_max = math.log(N)
253
- feats["entropy_norm"] = feats["dirac_entropy_final"] / S_max
254
- feats["entropy_abs_delta"] = abs(feats["dirac_entropy_delta"])
255
- feats["chirality_abs"] = abs(feats["dirac_chirality_final"])
256
- feats["energy_abs_mean"] = abs(feats["dirac_energy_mean"])
257
- feats["energy_std_sq"] = feats["dirac_energy_std"] ** 2
258
- feats["cosine_sq"] = feats["cosine_pa"] ** 2
259
- feats["len_log"] = math.log1p(feats["len_ratio"])
260
- feats["len_inv"] = 1.0 / (1.0 + feats["len_ratio"])
261
-
262
- return feats
263
-
264
-
265
- def features_to_vector(feats: Dict[str, float]) -> np.ndarray:
266
- keys = [
267
- "cosine_pa",
268
- "len_ratio",
269
- "dirac_entropy_final",
270
- "dirac_entropy_delta",
271
- "dirac_chirality_final",
272
- "dirac_energy_mean",
273
- "dirac_energy_std",
274
- "entropy_norm",
275
- "entropy_abs_delta",
276
- "chirality_abs",
277
- "energy_abs_mean",
278
- "energy_std_sq",
279
- "cosine_sq",
280
- "len_log",
281
- "len_inv",
282
- ]
283
- return np.array([feats[k] for k in keys], dtype=float)
284
 
 
 
285
 
286
- def compute_scores_srff_crff_ephi(prompt: str, answer: str):
287
- feats = compute_rrf_features(prompt, answer)
288
- x = features_to_vector(feats).reshape(1, -1)
289
 
290
- proba = meta_logit.predict_proba(x)[0]
291
- p_good = float(proba[1])
292
 
293
- SRRF = p_good
294
- CRRF = p_good * feats["cosine_pa"]
295
 
296
- S_max = math.log(N)
297
- norm_entropy = float(feats["dirac_entropy_final"] / S_max)
298
- E_phi = 0.5 * (SRRF + norm_entropy)
299
 
300
- scores = {
301
- "SRRF": SRRF,
302
- "CRRF": CRRF,
303
- "E_phi": E_phi,
304
- "p_good": p_good,
 
 
 
305
  }
306
- return scores, feats
307
-
308
 
309
- # ============================
310
- # Role profiles
311
- # ============================
312
 
313
- ROLE_PROFILES: Dict[str, Dict[str, float]] = {
314
- "default": {
315
- "SRRF": 1.0,
316
- "CRRF": 1.0,
317
- "E_phi": 1.0,
318
- },
319
- "creative": {
320
- "SRRF": 0.5,
321
- "CRRF": 0.5,
322
- "E_phi": 1.5,
323
- },
324
- "precise": {
325
- "SRRF": 1.0,
326
- "CRRF": 1.8,
327
- "E_phi": 0.4,
328
- },
329
- }
330
-
331
-
332
- def apply_role_profile(
333
- scores: Dict[str, float],
334
- role_name: Optional[str],
335
- ) -> Dict[str, Any]:
336
- if not role_name:
337
- role_name = "default"
338
-
339
- profile = ROLE_PROFILES.get(role_name, ROLE_PROFILES["default"])
340
-
341
- composite = 0.0
342
- weight_sum = 0.0
343
- for key, w in profile.items():
344
- if key in scores:
345
- composite += w * scores[key]
346
- weight_sum += abs(w)
347
-
348
- if weight_sum > 0.0:
349
- composite /= weight_sum
350
-
351
- return {
352
- "role": role_name,
353
- "weights": profile,
354
- "composite_score": composite,
355
- }
356
 
 
357
 
358
  # ============================
359
- # RRF Tutor: carga de dataset savant_rrf1
360
  # ============================
361
 
362
- print(f"🔄 [Startup] Cargando dataset para RRF Tutor: {RRF_TUTOR_DATASET_ID}...", flush=True)
363
- try:
364
- ds_rrf = load_dataset(RRF_TUTOR_DATASET_ID, split="train")
365
- ds_rrf = ds_rrf.filter(
366
- lambda ex: ex.get("prompt") is not None and ex.get("completion") is not None
367
- )
368
- print(f"✅ Dataset RRF Tutor cargado. Ejemplos útiles: {len(ds_rrf)}", flush=True)
369
- except Exception as e:
370
- print(f"❌ Error cargando dataset RRF Tutor: {e}", file=sys.stderr, flush=True)
371
- ds_rrf = None
372
-
373
- if ds_rrf is not None:
374
- print("🔄 [Startup] Construyendo textos y embeddings para RRF Tutor...", flush=True)
375
- rrf_corpus_texts: List[str] = []
376
- rrf_corpus_prompts: List[str] = []
377
- rrf_corpus_completions: List[str] = []
378
-
379
- for ex in ds_rrf:
380
- p = ex["prompt"]
381
- c = ex["completion"]
382
- rrf_corpus_prompts.append(p)
383
- rrf_corpus_completions.append(c)
384
- rrf_corpus_texts.append(p + "\n\n" + c)
385
-
386
- rrf_corpus_embeds = encoder.encode(
387
- rrf_corpus_texts,
388
- convert_to_numpy=True,
389
- show_progress_bar=True,
390
- normalize_embeddings=True,
391
- )
392
- print("✅ [RRF Tutor] Embeddings construidos.", flush=True)
393
- else:
394
- rrf_corpus_texts = []
395
- rrf_corpus_prompts = []
396
- rrf_corpus_completions = []
397
- rrf_corpus_embeds = np.zeros((0, 384), dtype=np.float32)
398
- print("⚠️ [RRF Tutor] Dataset no disponible, el endpoint devolverá error si se usa.", flush=True)
399
-
400
 
401
  # ============================
402
- # FastAPI app & modelos
403
  # ============================
404
 
405
  class EvaluateRequest(BaseModel):
406
  prompt: str
407
  answer: str
408
- model_label: Optional[str] = None
409
-
410
- class Config:
411
- protected_namespaces = () # evitar warning por model_label
412
 
413
 
414
  class EvaluateResponse(BaseModel):
 
415
  scores: Dict[str, float]
416
  features: Dict[str, float]
417
- sim_summary: Dict[str, Any]
418
- role_profile: Optional[Dict[str, Any]] = None
419
-
420
-
421
- class QualityRemoteRequest(EvaluateRequest):
422
- pass
423
-
424
-
425
- class RoleProfileInfo(BaseModel):
426
- name: str
427
- weights: Dict[str, float]
428
-
429
-
430
- class RoleProfilesResponse(BaseModel):
431
- roles: List[RoleProfileInfo]
432
 
433
 
434
  class RerankRequest(BaseModel):
435
- query: str = Field(..., description="Query de búsqueda o pregunta del usuario.")
436
- documents: List[str] = Field(..., description="Lista de documentos candidatos a rerankear.")
437
- alpha: float = Field(
438
- 0.2,
439
- description="Peso de la corrección log_rdf en el score_final. 0 = solo cosine, 1 = solo log_rdf.",
440
- )
441
- query_embedding_norm: bool = Field(
442
- True,
443
- description="Si True, normaliza el embedding de query (útil para cosine).",
444
- )
445
 
446
 
447
- class RerankDocumentResult(BaseModel):
448
- id: int = Field(..., description="Índice del documento en la lista de entrada.")
449
- score_cosine: float
450
- score_log_rdf: float
451
- score_final: float
452
  rank: int
453
 
454
 
455
  class RerankResponse(BaseModel):
456
  model_id: str
457
- alpha: float
458
- query_embedding_norm: bool
459
- results: List[RerankDocumentResult]
460
-
461
-
462
- class RRFTutorRequest(BaseModel):
463
- query: str = Field(..., description="Pregunta o fragmento de ecuación/idea RRF.")
464
- max_examples: int = Field(
465
- 3, ge=1, le=8,
466
- description="Número de ejemplos de savant_rrf1 a recuperar (1-8)."
467
- )
468
- include_raw_context: bool = Field(
469
- False,
470
- description="Si es true, devuelve los ejemplos recuperados."
471
- )
472
-
473
-
474
- class RetrievedExample(BaseModel):
475
- prompt: str
476
- completion: str
477
- score: float
478
-
479
-
480
- class RRFTutorResponse(BaseModel):
481
- answer: str
482
- retrieved: Optional[List[RetrievedExample]] = None
483
-
484
-
485
- app = FastAPI(
486
- title="Savant RRF Φ12.0 API",
487
- description="Dirac-Resonant conceptual quality layer + reranking + RRF Tutor.",
488
- version="1.1.0",
489
- )
490
-
491
 
492
  # ============================
493
- # Utilidades /v1/rerank
494
  # ============================
495
 
496
- def _compute_rerank_scores(query: str, docs: List[str], alpha: float, norm_query: bool) -> List[RerankDocumentResult]:
497
- q_emb = encoder.encode([query], convert_to_numpy=True, normalize_embeddings=norm_query)[0]
498
-
499
- results = []
500
- for idx, text in enumerate(docs):
501
- d_emb = encoder.encode([text], convert_to_numpy=True, normalize_embeddings=True)[0]
502
- score_cosine = float(np.dot(q_emb, d_emb))
503
-
504
- val = max(score_cosine, 0.0) + 1e-6
505
- score_log_rdf = float(np.log1p(val))
 
 
 
 
506
 
507
- score_final = (1.0 - alpha) * score_cosine + alpha * score_log_rdf
508
-
509
- results.append(
510
- {
511
- "id": idx,
512
- "score_cosine": score_cosine,
513
- "score_log_rdf": score_log_rdf,
514
- "score_final": score_final,
515
- }
516
- )
517
-
518
- results_sorted = sorted(results, key=lambda r: r["score_final"], reverse=True)
519
- reranked = []
520
- for rank, r in enumerate(results_sorted, start=1):
521
- reranked.append(
522
- RerankDocumentResult(
523
- id=r["id"],
524
- score_cosine=r["score_cosine"],
525
- score_log_rdf=r["score_log_rdf"],
526
- score_final=r["score_final"],
527
- rank=rank,
528
- )
529
- )
530
- return reranked
531
 
 
 
 
532
 
533
  # ============================
534
- # Utilidades /v1/rrf_tutor
535
  # ============================
536
 
537
- def rrf_tutor_retrieve_examples(query: str, top_k: int = 3):
538
- if rrf_corpus_embeds is None or len(rrf_corpus_embeds) == 0:
539
- raise RuntimeError("Embeddings de RRF Tutor no están disponibles.")
540
-
541
- q_emb = encoder.encode([query], convert_to_numpy=True, normalize_embeddings=True)[0]
542
- sims = np.dot(rrf_corpus_embeds, q_emb)
543
-
544
- top_k = min(top_k, len(rrf_corpus_embeds))
545
- top_idx = np.argsort(-sims)[:top_k]
546
-
547
- results = []
548
- for idx in top_idx:
549
- results.append(
550
- {
551
- "idx": int(idx),
552
- "score": float(sims[idx]),
553
- "prompt": rrf_corpus_prompts[idx],
554
- "completion": rrf_corpus_completions[idx],
555
- }
556
- )
557
- return results
558
-
559
 
560
- def rrf_tutor_build_answer(query: str, retrieved_examples):
561
- if not retrieved_examples:
562
- return (
563
- "No encontré ejemplos relevantes en el dataset RRF Tutor para tu consulta. "
564
- "Intenta reformular la pregunta o revisar la configuración del dataset."
565
- )
566
 
567
- best = retrieved_examples[0]
568
- base_completion = best["completion"]
 
 
 
569
 
570
- answer = (
571
- "🔎 Respuesta basada en el ejemplo más cercano del corpus RRF:\n\n"
572
- f"{base_completion}\n\n"
573
- "💡 Nota: Esta es una versión mínima que reutiliza directamente la 'completion' "
574
- "del ejemplo más similar en savant_rrf1. En una versión extendida, aquí se "
575
- "conectaría un LLM pequeño (TinyLlama, etc.) que use varios ejemplos como "
576
- "contexto para generar una explicación personalizada a tu `query`."
577
  )
578
- return answer
579
 
580
 
 
 
 
 
581
  # ============================
582
- # Endpoints
583
  # ============================
584
 
585
- @app.get("/")
586
- def root():
587
- return {"message": "Savant RRF Φ12.0 API running", "docs": "/docs"}
 
588
 
 
 
 
 
589
 
590
- @app.get("/health")
591
- def health():
592
- return {
593
- "status": "ok",
594
- "encoder_model_id": ENCODER_MODEL_ID,
595
- "meta_logit_filename": META_LOGIT_FILENAME,
596
- "N_sites": N,
597
- }
598
 
 
 
599
 
600
- @app.get("/roles", response_model=RoleProfilesResponse)
601
- def list_roles():
602
- roles = [
603
- RoleProfileInfo(name=name, weights=weights)
604
- for name, weights in ROLE_PROFILES.items()
605
- ]
606
- return RoleProfilesResponse(roles=roles)
607
 
608
-
609
- @app.post("/evaluate", response_model=EvaluateResponse)
610
- def evaluate(req: EvaluateRequest):
611
- try:
612
- scores, feats = compute_scores_srff_crff_ephi(req.prompt, req.answer)
613
- role_profile = apply_role_profile(scores, req.model_label)
614
-
615
- H = build_dirac_hamiltonian(
616
- m=0.25, v=1.0, sigma=0.618,
617
- alpha_log=0.10, q=1.0,
618
- flux_vector=(0.0, 0.0, 0.0),
619
- gauge_scale=0.0,
620
- )
621
- rng = np.random.default_rng(
622
- abs(hash(req.prompt + req.answer + "sim")) % (2 ** 32)
623
- )
624
- vec = rng.normal(0, 1, (2 * N,)) + 1j * rng.normal(0, 1, (2 * N,))
625
- vec /= np.sqrt(np.vdot(vec, vec))
626
- psi0 = vec
627
- sim = evolve_dirac_shell(psi0, H, dt=0.05, steps=60, record_every=20)
628
-
629
- sim_summary = {
630
- "entropy_initial": float(sim["entropy"][0]),
631
- "entropy_final": float(sim["entropy"][-1]),
632
- "chirality_initial": float(sim["chirality"][0]),
633
- "chirality_final": float(sim["chirality"][-1]),
634
- "energy_mean": float(np.mean(sim["energy"])),
635
- "energy_std": float(np.std(sim["energy"])),
636
- "N_sites": int(N),
637
- }
638
-
639
- return EvaluateResponse(
640
- scores=scores,
641
- features=feats,
642
- sim_summary=sim_summary,
643
- role_profile=role_profile,
644
  )
645
- except Exception as e:
646
- print(f"❌ [Runtime] Error en /evaluate: {e}", file=sys.stderr, flush=True)
647
- raise HTTPException(status_code=500, detail="Internal server error")
648
-
649
-
650
- @app.post("/quality_remote", response_model=EvaluateResponse)
651
- def quality_remote(req: QualityRemoteRequest):
652
- return evaluate(req)
653
-
654
-
655
- @app.post("/quality", response_model=EvaluateResponse)
656
- def quality_alias(req: QualityRemoteRequest):
657
- """Alias de /evaluate para compatibilidad con clientes anteriores."""
658
- return evaluate(req)
659
-
660
-
661
- @app.post("/v1/rerank", response_model=RerankResponse)
662
- def rerank_endpoint(req: RerankRequest):
663
- results = _compute_rerank_scores(
664
- query=req.query,
665
- docs=req.documents,
666
- alpha=req.alpha,
667
- norm_query=req.query_embedding_norm,
668
- )
669
 
670
  return RerankResponse(
671
  model_id=ENCODER_MODEL_ID,
672
- alpha=req.alpha,
673
- query_embedding_norm=req.query_embedding_norm,
674
  results=results,
675
  )
676
-
677
-
678
- @app.post("/v1/rrf_tutor", response_model=RRFTutorResponse)
679
- def rrf_tutor_endpoint(body: RRFTutorRequest):
680
- if not body.query or not body.query.strip():
681
- raise HTTPException(status_code=400, detail="El campo 'query' no puede estar vacío.")
682
-
683
- if ds_rrf is None or rrf_corpus_embeds is None or len(rrf_corpus_embeds) == 0:
684
- raise HTTPException(
685
- status_code=500,
686
- detail="El dataset/embeddings de RRF Tutor no están disponibles en este momento.",
687
- )
688
-
689
- try:
690
- retrieved = rrf_tutor_retrieve_examples(body.query, top_k=body.max_examples)
691
- except Exception as e:
692
- raise HTTPException(
693
- status_code=500,
694
- detail=f"Error interno recuperando ejemplos RRF Tutor: {e}",
695
- )
696
-
697
- answer = rrf_tutor_build_answer(body.query, retrieved)
698
-
699
- resp = RRFTutorResponse(answer=answer)
700
-
701
- if body.include_raw_context:
702
- resp.retrieved = [
703
- RetrievedExample(
704
- prompt=ex["prompt"],
705
- completion=ex["completion"],
706
- score=ex["score"],
707
- )
708
- for ex in retrieved
709
- ]
710
-
711
- return resp
 
1
+ import os, sys, math
 
 
2
  from typing import Optional, Dict, Any, List
3
 
4
  import numpy as np
5
  from numpy.linalg import norm
 
 
6
  from fastapi import FastAPI, HTTPException
7
  from pydantic import BaseModel, Field
8
 
9
  from sentence_transformers import SentenceTransformer
10
  from huggingface_hub import hf_hub_download
 
11
  import joblib
12
 
13
  # ============================
14
+ # CONFIG
15
  # ============================
16
 
17
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
18
 
19
+ ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
20
+ META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogicV2"
21
+ META_LOGIT_FILENAME = "logreg_rrf_savant.joblib"
22
+
23
+ MAX_PROMPT_CHARS = 8000
24
+ MAX_ANSWER_CHARS = 12000
25
+ MAX_DOCS = 50
26
+ MAX_DOC_CHARS = 6000
27
+
28
+ PHI_NODES = [
29
+ "Φ0_seed",
30
+ "Φ1_geometric",
31
+ "Φ2_gauge_dirac",
32
+ "Φ3_log_gravity",
33
+ "Φ4_resonance",
34
+ "Φ5_memory_symbiosis",
35
+ "Φ6_alignment",
36
+ "Φ7_meta_agi",
37
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  # ============================
40
+ # STARTUP: MODELS
41
  # ============================
42
 
43
+ print("🔄 Loading encoder...", flush=True)
44
+ encoder = SentenceTransformer(ENCODER_MODEL_ID)
45
+ print("✅ Encoder loaded", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ print("🔄 Loading meta-logit V2...", flush=True)
48
+ meta_logit_path = hf_hub_download(
49
+ repo_id=META_LOGIT_REPO,
50
+ filename=META_LOGIT_FILENAME,
51
+ token=HF_TOKEN or None,
52
+ )
53
+ meta_logit = joblib.load(meta_logit_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ EXPECTED_FEATURES = getattr(meta_logit, "n_features_in_", 15)
56
+ if EXPECTED_FEATURES != 15:
57
+ raise RuntimeError(f"Meta-logit expects {EXPECTED_FEATURES} features, expected 15.")
 
 
 
 
 
58
 
59
+ print("✅ Meta-logit loaded (15D)", flush=True)
60
 
61
  # ============================
62
+ # META-STATE FEATURE EXTRACTION
63
  # ============================
64
 
65
  def get_embedding(text: str) -> np.ndarray:
66
+ return encoder.encode(
67
+ [text],
68
+ convert_to_numpy=True,
69
+ normalize_embeddings=True,
70
+ )[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ def spectral_features(emb: np.ndarray) -> Dict[str, float]:
74
+ fft = np.fft.rfft(emb)
75
+ power = np.abs(fft) ** 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ total = power.sum() + 1e-12
78
+ dominant_idx = int(np.argmax(power))
79
 
80
+ phi = float(np.clip(total / (total + 1.0), 0.0, 1.0))
81
+ omega = float(np.clip(dominant_idx / len(power), 0.0, 1.0))
 
82
 
83
+ S_RRF = float(np.mean(np.diff(power)))
84
+ C_RRF = float(power[dominant_idx] / total)
85
 
86
+ coherence = float(0.5 * (1.0 - np.std(power) / (np.mean(power) + 1e-12)) + 0.5 * C_RRF)
 
87
 
88
+ hamiltonian_energy = float(np.dot(emb, emb))
89
+ dominant_frequency = float(dominant_idx)
 
90
 
91
+ return {
92
+ "phi": phi,
93
+ "omega": omega,
94
+ "coherence": coherence,
95
+ "S_RRF": S_RRF,
96
+ "C_RRF": C_RRF,
97
+ "hamiltonian_energy": hamiltonian_energy,
98
+ "dominant_frequency": dominant_frequency,
99
  }
 
 
100
 
 
 
 
101
 
102
+ def closest_phi_node(feats: Dict[str, float]) -> int:
103
+ # Deterministic ontology mapping
104
+ if feats["coherence"] > 0.85 and feats["phi"] > 0.6:
105
+ return 4 # Φ4_resonance
106
+ if feats["hamiltonian_energy"] > 50:
107
+ return 2 # Φ2_gauge_dirac
108
+ if feats["omega"] < 0.2:
109
+ return 0 # Φ0_seed
110
+ if feats["coherence"] < 0.4:
111
+ return 5 # Φ5_memory_symbiosis
112
+ if feats["phi"] < 0.3:
113
+ return 6 # Φ6_alignment
114
+ return 7 # Φ7_meta_agi
115
+
116
+
117
+ def rrf_state_to_vector(prompt: str, answer: str) -> np.ndarray:
118
+ emb = get_embedding(prompt + "\n" + answer)
119
+ feats = spectral_features(emb)
120
+
121
+ phi_idx = closest_phi_node(feats)
122
+ phi_one_hot = [1.0 if i == phi_idx else 0.0 for i in range(8)]
123
+
124
+ vector = [
125
+ feats["phi"],
126
+ feats["omega"],
127
+ feats["coherence"],
128
+ feats["S_RRF"],
129
+ feats["C_RRF"],
130
+ feats["hamiltonian_energy"],
131
+ feats["dominant_frequency"],
132
+ *phi_one_hot,
133
+ ]
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ return np.array(vector, dtype=float), feats, PHI_NODES[phi_idx]
136
 
137
  # ============================
138
+ # FASTAPI
139
  # ============================
140
 
141
+ app = FastAPI(
142
+ title="Savant RRF Φ12.0 API",
143
+ version="2.0.0",
144
+ description="Meta-state RRF quality evaluation + rerank",
145
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  # ============================
148
+ # SCHEMAS
149
  # ============================
150
 
151
  class EvaluateRequest(BaseModel):
152
  prompt: str
153
  answer: str
 
 
 
 
154
 
155
 
156
  class EvaluateResponse(BaseModel):
157
+ p_good: float
158
  scores: Dict[str, float]
159
  features: Dict[str, float]
160
+ phi_node: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
 
163
  class RerankRequest(BaseModel):
164
+ query: str
165
+ documents: List[str]
166
+ alpha: float = 0.2
 
 
 
 
 
 
 
167
 
168
 
169
+ class RerankDocument(BaseModel):
170
+ id: int
171
+ score: float
 
 
172
  rank: int
173
 
174
 
175
  class RerankResponse(BaseModel):
176
  model_id: str
177
+ results: List[RerankDocument]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  # ============================
180
+ # MANIFEST / HEALTH
181
  # ============================
182
 
183
+ @app.get("/manifest")
184
+ def manifest():
185
+ return {
186
+ "model": "RRFSavantMetaLogicV2",
187
+ "version": "Φ12.0",
188
+ "encoder": ENCODER_MODEL_ID,
189
+ "meta_logit_repo": META_LOGIT_REPO,
190
+ "features": 15,
191
+ "feature_order": [
192
+ "phi", "omega", "coherence", "S_RRF", "C_RRF",
193
+ "hamiltonian_energy", "dominant_frequency",
194
+ *PHI_NODES
195
+ ],
196
+ }
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ @app.get("/health")
200
+ def health():
201
+ return {"status": "ok"}
202
 
203
  # ============================
204
+ # /EVALUATE
205
  # ============================
206
 
207
+ @app.post("/evaluate", response_model=EvaluateResponse)
208
+ def evaluate(req: EvaluateRequest):
209
+ if len(req.prompt) > MAX_PROMPT_CHARS or len(req.answer) > MAX_ANSWER_CHARS:
210
+ raise HTTPException(413, "Payload too large")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ x, feats, phi_node = rrf_state_to_vector(req.prompt, req.answer)
213
+ proba = meta_logit.predict_proba(x.reshape(1, -1))[0]
214
+ p_good = float(proba[1])
 
 
 
215
 
216
+ scores = {
217
+ "SRRF": p_good,
218
+ "CRRF": p_good * feats["coherence"],
219
+ "E_phi": 0.5 * (p_good + feats["phi"]),
220
+ }
221
 
222
+ return EvaluateResponse(
223
+ p_good=p_good,
224
+ scores=scores,
225
+ features=feats,
226
+ phi_node=phi_node,
 
 
227
  )
 
228
 
229
 
230
+ @app.post("/quality", response_model=EvaluateResponse)
231
+ def quality_alias(req: EvaluateRequest):
232
+ return evaluate(req)
233
+
234
  # ============================
235
+ # /v1/rerank (BATCHED)
236
  # ============================
237
 
238
+ @app.post("/v1/rerank", response_model=RerankResponse)
239
+ def rerank(req: RerankRequest):
240
+ if len(req.documents) > MAX_DOCS:
241
+ raise HTTPException(413, "Too many documents")
242
 
243
+ texts = [req.query] + req.documents
244
+ for d in req.documents:
245
+ if len(d) > MAX_DOC_CHARS:
246
+ raise HTTPException(413, "Document too large")
247
 
248
+ embs = encoder.encode(
249
+ texts,
250
+ convert_to_numpy=True,
251
+ normalize_embeddings=True,
252
+ )
 
 
 
253
 
254
+ q_emb = embs[0]
255
+ d_embs = embs[1:]
256
 
257
+ scores = d_embs @ q_emb
258
+ ranked_idx = np.argsort(-scores)
 
 
 
 
 
259
 
260
+ results = [
261
+ RerankDocument(
262
+ id=int(i),
263
+ score=float(scores[i]),
264
+ rank=r + 1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  )
266
+ for r, i in enumerate(ranked_idx)
267
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  return RerankResponse(
270
  model_id=ENCODER_MODEL_ID,
 
 
271
  results=results,
272
  )