antonypamo commited on
Commit
0b79761
·
verified ·
1 Parent(s): 54d2399

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +580 -4
app.py CHANGED
@@ -1,4 +1,580 @@
1
- from fastapi import FastAPI
2
- app=FastAPI()
3
- @app.get('/')
4
- def root(): return {'status':'ok'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================================
2
+ # Savant RRF Φ12.0 — app.py (AGIRRFCore-aligned, HARDENED)
3
+ # Uses the same AGIRRFCore logic as RRFSavant_AGI_Core_Colab
4
+ # ======================================================
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ import os, json, math, time
11
+ from typing import Optional, Dict, Any, List, Tuple
12
+
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn as nn
16
+
17
+ from fastapi import FastAPI, HTTPException
18
+ from pydantic import BaseModel, Field, ConfigDict
19
+
20
+ from sentence_transformers import SentenceTransformer
21
+ from huggingface_hub import hf_hub_download
22
+ import joblib
23
+
24
+
25
+ # ======================================================
26
+ # 0) Hardening limits
27
+ # ======================================================
28
+
29
+ MAX_PROMPT_CHARS = int(os.environ.get("MAX_PROMPT_CHARS", "8000"))
30
+ MAX_ANSWER_CHARS = int(os.environ.get("MAX_ANSWER_CHARS", "12000"))
31
+ MAX_DOCS = int(os.environ.get("MAX_DOCS", "50"))
32
+ MAX_DOC_CHARS = int(os.environ.get("MAX_DOC_CHARS", "6000"))
33
+
34
+
35
+ # ======================================================
36
+ # 1) MANIFEST
37
+ # ======================================================
38
+
39
+ DEFAULT_MANIFEST = {
40
+ "version": "Φ12.0",
41
+ "project": "Savant RRF API & Meta-Logic Suite",
42
+ "owner": "Antony Padilla Morales",
43
+ "status": "fallback_default",
44
+ }
45
+
46
+ MANIFEST_PATH = Path(__file__).parent / "savant_rrf_api_manifest_phi12.json"
47
+
48
+ def load_manifest_file() -> Dict[str, Any]:
49
+ if MANIFEST_PATH.exists():
50
+ try:
51
+ print(f"[Manifest] Loading from {MANIFEST_PATH}", flush=True)
52
+ return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
53
+ except Exception as e:
54
+ print(f"[Manifest] Invalid JSON: {e}", flush=True)
55
+ print("[Manifest] Using DEFAULT_MANIFEST", flush=True)
56
+ return DEFAULT_MANIFEST
57
+
58
+ manifest_data = load_manifest_file()
59
+ print("[Manifest] version:", manifest_data.get("version"), flush=True)
60
+
61
+
62
+ # ======================================================
63
+ # 2) Global config
64
+ # ======================================================
65
+
66
+ HF_TOKEN = os.environ.get("HF_TOKEN", "") # set in Spaces secrets
67
+ if HF_TOKEN:
68
+ os.environ["HF_TOKEN"] = HF_TOKEN
69
+
70
+ ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
71
+ META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogicV2"
72
+ META_LOGIT_FILENAME = "logreg_rrf_savant.joblib"
73
+
74
+ RRF_DATASET_REPO = "antonypamo/savant_rrf1_curated"
75
+
76
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
77
+ st_device = "cuda" if torch.cuda.is_available() else "cpu"
78
+
79
+
80
+ def _hf_download_safe(
81
+ repo_id: str,
82
+ filename: str,
83
+ *,
84
+ repo_type: Optional[str] = None,
85
+ token: Optional[str] = None,
86
+ ) -> Optional[str]:
87
+ """
88
+ Robust HF download:
89
+ - returns local path or None
90
+ - prints actionable errors (401/private/gated/missing)
91
+ """
92
+ try:
93
+ return hf_hub_download(
94
+ repo_id=repo_id,
95
+ filename=filename,
96
+ repo_type=repo_type,
97
+ token=token or None,
98
+ )
99
+ except Exception as e:
100
+ msg = str(e)
101
+ if "401" in msg or "Unauthorized" in msg:
102
+ print(f"❌ [HF] 401 Unauthorized downloading {repo_id}/{filename}. "
103
+ f"Repo may be private/gated or HF_TOKEN missing/invalid.", flush=True)
104
+ elif "RepositoryNotFoundError" in msg or "404" in msg:
105
+ print(f"❌ [HF] Repo or file not found: {repo_id}/{filename}", flush=True)
106
+ else:
107
+ print(f"⚠️ [HF] Download failed: {repo_id}/{filename} | {e}", flush=True)
108
+ return None
109
+
110
+
111
+ def hf_dataset_path(filename: str) -> Optional[str]:
112
+ return _hf_download_safe(
113
+ repo_id=RRF_DATASET_REPO,
114
+ filename=filename,
115
+ repo_type="dataset",
116
+ token=HF_TOKEN if HF_TOKEN else None,
117
+ )
118
+
119
+
120
+ # ======================================================
121
+ # 3) Optional artifacts (dataset assets)
122
+ # ======================================================
123
+
124
+ SAVANT_CNN_PATH = hf_dataset_path("savant_cnn.pt")
125
+ RRF_NODES_PATH = hf_dataset_path("rrf_nodes.pt")
126
+ RRF_TUTOR_JSONL = hf_dataset_path("rrf_tutor_curated.jsonl")
127
+
128
+
129
+ # ======================================================
130
+ # 4) Savant CNN (optional)
131
+ # ======================================================
132
+
133
+ class SavantCNN(nn.Module):
134
+ def __init__(self):
135
+ super().__init__()
136
+ self.conv1 = nn.Conv1d(1, 32, 3, padding=1)
137
+ self.conv2 = nn.Conv1d(32, 64, 3, padding=1)
138
+ self.conv3 = nn.Conv1d(64, 128, 3, padding=1)
139
+ self.pool = nn.AdaptiveAvgPool1d(4)
140
+ self.fc = nn.Linear(512, 64)
141
+
142
+ def forward(self, x):
143
+ x = torch.relu(self.conv1(x))
144
+ x = torch.relu(self.conv2(x))
145
+ x = torch.relu(self.conv3(x))
146
+ x = self.pool(x)
147
+ x = x.view(x.size(0), -1)
148
+ return self.fc(x)
149
+
150
+
151
+ savant_cnn = None
152
+ if SAVANT_CNN_PATH:
153
+ try:
154
+ savant_cnn = SavantCNN()
155
+ savant_cnn.load_state_dict(torch.load(SAVANT_CNN_PATH, map_location=device))
156
+ savant_cnn.to(device).eval()
157
+ print("✅ Savant CNN loaded", flush=True)
158
+ except Exception as e:
159
+ print(f"⚠️ CNN load failed: {e}", flush=True)
160
+
161
+ rrf_nodes = None
162
+ if RRF_NODES_PATH:
163
+ try:
164
+ rrf_nodes = torch.load(RRF_NODES_PATH, map_location=device)
165
+ print("✅ RRF nodes loaded", flush=True)
166
+ except Exception as e:
167
+ print(f"⚠️ RRF nodes load failed: {e}", flush=True)
168
+
169
+
170
+ # ======================================================
171
+ # 5) Φ-node ontology (8 nodes -> one-hot 8)
172
+ # ======================================================
173
+
174
+ @dataclass
175
+ class PhiNode:
176
+ name: str
177
+ description: str
178
+ tags: List[str] = field(default_factory=list)
179
+ embedding: Optional[np.ndarray] = None # runtime only
180
+
181
+ PHI_NODES: List[PhiNode] = [
182
+ PhiNode("Φ0_seed", "Genesis seed, core identity and origin.", ["genesis","identity","anchor"]),
183
+ PhiNode("Φ1_relation", "Relational bonding, dialogue, social meaning.", ["relation","dialogue"]),
184
+ PhiNode("Φ2_resonance", "Signal resonance, harmonic alignment, coherence lift.", ["resonance","harmonics"]),
185
+ PhiNode("Φ3_memory", "Memory consolidation, retrieval, indexing.", ["memory","retrieval"]),
186
+ PhiNode("Φ4_logic", "Logical rigor, constraints, verification.", ["logic","verification"]),
187
+ PhiNode("Φ5_creative", "Creative synthesis, metaphor, generative jumps.", ["creative","synthesis"]),
188
+ PhiNode("Φ6_alignment", "Ethical alignment and safety constraints.", ["alignment","ethics"]),
189
+ PhiNode("Φ7_meta_agi", "Meta-orchestrator that evaluates and routes flows.", ["meta","orchestration"]),
190
+ ]
191
+ PHI_NAME_TO_IDX = {n.name: i for i, n in enumerate(PHI_NODES)}
192
+
193
+
194
+ def phi_nodes_public() -> List[Dict[str, Any]]:
195
+ # JSON-safe version (no embeddings)
196
+ return [{"name": n.name, "description": n.description, "tags": n.tags} for n in PHI_NODES]
197
+
198
+
199
+ # ======================================================
200
+ # 6) CoherenceModel (stable S_RRF + C_RRF)
201
+ # ======================================================
202
+
203
+ class CoherenceModel:
204
+ def __init__(self, eps: float = 1e-9):
205
+ self.eps = eps
206
+
207
+ def compute(self, vec: np.ndarray) -> Tuple[float, float]:
208
+ v = np.asarray(vec, dtype=float).ravel()
209
+ n = len(v)
210
+ if n < 4:
211
+ return 0.0, 0.0
212
+
213
+ spectrum = np.fft.rfft(v)
214
+ power = (np.abs(spectrum) ** 2).astype(float)
215
+ freqs = np.fft.rfftfreq(n, d=1.0).astype(float)
216
+
217
+ total_power = float(power.sum()) + self.eps
218
+
219
+ # C_RRF: concentration in dominant frequency
220
+ C_RRF = float(power.max() / total_power)
221
+
222
+ # S_RRF: prefer lower average frequency
223
+ f_mean = float((freqs * power).sum() / total_power)
224
+ f_max = float(freqs.max()) + self.eps
225
+ S_RRF = float(1.0 - min(1.0, f_mean / f_max))
226
+
227
+ return S_RRF, C_RRF
228
+
229
+ coherence_model = CoherenceModel()
230
+
231
+
232
+ # ======================================================
233
+ # 7) AGIRRFCore (aligned)
234
+ # ======================================================
235
+
236
+ class AGIRRFCore:
237
+ def __init__(
238
+ self,
239
+ phi_nodes: List[PhiNode],
240
+ coherence_model: Optional[CoherenceModel] = None,
241
+ st_model_name: str = ENCODER_MODEL_ID,
242
+ ):
243
+ self.phi_nodes = phi_nodes
244
+ self.coherence_model = coherence_model
245
+
246
+ print(f"🔄 Loading sentence-transformer: {st_model_name} on {st_device} ...", flush=True)
247
+ self.embedder = SentenceTransformer(st_model_name, device=st_device)
248
+ print("✅ Embedder loaded", flush=True)
249
+
250
+ self._embed_phi_nodes()
251
+
252
+ def _embed_text(self, text: str) -> np.ndarray:
253
+ return self.embedder.encode([text], convert_to_numpy=True)[0]
254
+
255
+ def _embed_phi_nodes(self):
256
+ texts = [f"{n.name}: {n.description} | tags: {', '.join(n.tags)}" for n in self.phi_nodes]
257
+ embs = self.embedder.encode(texts, convert_to_numpy=True)
258
+ for node, emb in zip(self.phi_nodes, embs):
259
+ node.embedding = emb
260
+ print(f"✅ Embedded {len(self.phi_nodes)} Φ-nodes.", flush=True)
261
+
262
+ def _dominant_frequency(self, vec: np.ndarray) -> float:
263
+ v = np.asarray(vec, dtype=float).ravel()
264
+ if len(v) < 4:
265
+ return 0.0
266
+ spectrum = np.fft.rfft(v)
267
+ power = np.abs(spectrum) ** 2
268
+ freqs = np.fft.rfftfreq(len(v), d=1.0)
269
+ idx = int(np.argmax(power))
270
+ return float(freqs[idx])
271
+
272
+ def _phi_omega(self, energy: float, dom_freq: float) -> Tuple[float, float]:
273
+ phi = 1.0 - math.exp(-float(energy)) # saturating
274
+ omega = math.tanh(dom_freq * 10.0) # saturating
275
+ return float(phi), float(omega)
276
+
277
+ def _closest_phi_node(self, vec: np.ndarray) -> Tuple[str, float]:
278
+ if not self.phi_nodes or self.phi_nodes[0].embedding is None:
279
+ return "unknown", 0.0
280
+ v = np.asarray(vec, dtype=float).ravel()
281
+ v_norm = np.linalg.norm(v) + 1e-9
282
+ best_name, best_cos = "unknown", -1.0
283
+ for node in self.phi_nodes:
284
+ e = node.embedding
285
+ if e is None:
286
+ continue
287
+ cos = float(np.dot(v, e) / (v_norm * (np.linalg.norm(e) + 1e-9)))
288
+ if cos > best_cos:
289
+ best_cos = cos
290
+ best_name = node.name
291
+ return best_name, best_cos
292
+
293
+ def analyze(self, text: str, context_label: str = "query") -> Dict[str, Any]:
294
+ vec = self._embed_text(text)
295
+
296
+ energy = float(np.dot(vec, vec))
297
+ dom_freq = self._dominant_frequency(vec)
298
+ phi, omega = self._phi_omega(energy, dom_freq)
299
+
300
+ if self.coherence_model is not None:
301
+ S_RRF, C_RRF = self.coherence_model.compute(vec)
302
+ else:
303
+ S_RRF, C_RRF = 0.0, 0.0
304
+
305
+ coherence = 0.5 * float(S_RRF) + 0.5 * float(C_RRF)
306
+ closest_name, closest_cos = self._closest_phi_node(vec)
307
+
308
+ return {
309
+ "context": context_label,
310
+ "phi": phi,
311
+ "omega": omega,
312
+ "coherence": float(coherence),
313
+ "S_RRF": float(S_RRF),
314
+ "C_RRF": float(C_RRF),
315
+ "hamiltonian_energy": float(energy),
316
+ "dominant_frequency": float(dom_freq),
317
+ "closest_phi_node": closest_name,
318
+ "closest_phi_cos": float(closest_cos),
319
+ "timestamp": float(time.time()),
320
+ }
321
+
322
+
323
+ agirrf_core = AGIRRFCore(
324
+ phi_nodes=PHI_NODES,
325
+ coherence_model=coherence_model,
326
+ st_model_name=ENCODER_MODEL_ID,
327
+ )
328
+
329
+
330
+ # ======================================================
331
+ # 8) Load Meta-Logit (15D)
332
+ # ======================================================
333
+
334
+ print("🔄 Loading meta-logit...", flush=True)
335
+ meta_logit_path = _hf_download_safe(
336
+ repo_id=META_LOGIT_REPO,
337
+ filename=META_LOGIT_FILENAME,
338
+ token=HF_TOKEN if HF_TOKEN else None,
339
+ )
340
+ if not meta_logit_path:
341
+ raise RuntimeError(
342
+ f"Meta-logit not available. Check repo_id={META_LOGIT_REPO}, "
343
+ f"filename={META_LOGIT_FILENAME}, and HF_TOKEN if private."
344
+ )
345
+ meta_logit = joblib.load(meta_logit_path)
346
+
347
+ EXPECTED_FEATURES = getattr(meta_logit, "n_features_in_", 15)
348
+ if EXPECTED_FEATURES != 15:
349
+ raise RuntimeError(f"Meta-logit expects {EXPECTED_FEATURES} features, expected 15.")
350
+ print("✅ Meta-logit ready (15D)", flush=True)
351
+
352
+
353
+ # ======================================================
354
+ # 9) Feature mapping (7 + one-hot 8 = 15)
355
+ # ======================================================
356
+
357
+ def rrf_state_to_features(state: Dict[str, Any]) -> np.ndarray:
358
+ phi = float(state.get("phi", 0.0))
359
+ omega = float(state.get("omega", 0.0))
360
+ coh = float(state.get("coherence", 0.0))
361
+ S_RRF = float(state.get("S_RRF", 0.0))
362
+ C_RRF = float(state.get("C_RRF", 0.0))
363
+ E_H = float(state.get("hamiltonian_energy", 0.0))
364
+ dom_f = float(state.get("dominant_frequency", 0.0))
365
+
366
+ phi_name = state.get("closest_phi_node", "unknown")
367
+ phi_onehot = np.zeros(len(PHI_NODES), dtype=float)
368
+ idx = PHI_NAME_TO_IDX.get(phi_name)
369
+ if idx is not None:
370
+ phi_onehot[idx] = 1.0
371
+
372
+ base = np.array([phi, omega, coh, S_RRF, C_RRF, E_H, dom_f], dtype=float)
373
+ return np.concatenate([base, phi_onehot], axis=0)
374
+
375
+
376
+ # ======================================================
377
+ # 10) Core scoring (prompt, answer)
378
+ # ======================================================
379
+
380
+ def _embed_norm(text: str) -> np.ndarray:
381
+ return agirrf_core.embedder.encode([text], convert_to_numpy=True, normalize_embeddings=True)[0]
382
+
383
+ def compute_scores(prompt: str, answer: str) -> Dict[str, Any]:
384
+ prompt = prompt or ""
385
+ answer = answer or ""
386
+ if not prompt.strip() or not answer.strip():
387
+ raise ValueError("Empty prompt/answer")
388
+
389
+ if len(prompt) > MAX_PROMPT_CHARS or len(answer) > MAX_ANSWER_CHARS:
390
+ raise HTTPException(status_code=413, detail="Payload too large")
391
+
392
+ # extra signal: cosine(prompt, answer)
393
+ e_p = _embed_norm(prompt)
394
+ e_a = _embed_norm(answer)
395
+ cosine = float(np.dot(e_p, e_a))
396
+
397
+ # stable single-state features on combined QA text
398
+ qa_text = f"Q: {prompt}\nA: {answer}"
399
+ state = agirrf_core.analyze(qa_text, context_label="qa")
400
+ feats = rrf_state_to_features(state).reshape(1, -1)
401
+
402
+ p_good = float(meta_logit.predict_proba(feats)[0][1])
403
+
404
+ SRRF = p_good
405
+ CRRF = p_good * cosine
406
+ E_phi = 0.5 * (p_good + abs(cosine))
407
+
408
+ return {
409
+ "p_good": p_good,
410
+ "SRRF": SRRF,
411
+ "CRRF": CRRF,
412
+ "E_phi": E_phi,
413
+ "cosine": cosine,
414
+
415
+ # debug/state exposure (key for Savant)
416
+ "phi": float(state["phi"]),
417
+ "omega": float(state["omega"]),
418
+ "coherence": float(state["coherence"]),
419
+ "S_RRF": float(state["S_RRF"]),
420
+ "C_RRF": float(state["C_RRF"]),
421
+ "hamiltonian_energy": float(state["hamiltonian_energy"]),
422
+ "dominant_frequency": float(state["dominant_frequency"]),
423
+ "closest_phi_node": state["closest_phi_node"],
424
+ "closest_phi_cos": float(state["closest_phi_cos"]),
425
+ }
426
+
427
+
428
+ # ======================================================
429
+ # 11) FastAPI models
430
+ # ======================================================
431
+
432
+ class EvaluateRequest(BaseModel):
433
+ model_config = ConfigDict(protected_namespaces=())
434
+ prompt: str
435
+ answer: str
436
+ model_label: Optional[str] = None # reserved for future routing
437
+
438
+ class EvaluateResponse(BaseModel):
439
+ scores: Dict[str, Any]
440
+ manifest_version: str
441
+
442
+ class PredictRequest(BaseModel):
443
+ features: List[float] = Field(..., min_length=15, max_length=15)
444
+
445
+ class PredictResponse(BaseModel):
446
+ p_good: float
447
+
448
+ class RerankRequest(BaseModel):
449
+ query: str
450
+ documents: List[str]
451
+ alpha: float = 0.2 # kept for compatibility (not used in cosine rerank)
452
+
453
+ class RerankDocument(BaseModel):
454
+ id: int
455
+ score: float
456
+ rank: int
457
+
458
+ class RerankResponse(BaseModel):
459
+ model_config = ConfigDict(protected_namespaces=())
460
+ model_id: str
461
+ results: List[RerankDocument]
462
+
463
+
464
+ # ======================================================
465
+ # 12) FastAPI app
466
+ # ======================================================
467
+
468
+ app = FastAPI(
469
+ title="Savant RRF Φ12.0 API",
470
+ version="1.2.1",
471
+ description="AGIRRFCore-aligned Meta-Logic, Reranking & Quality Evaluation",
472
+ )
473
+
474
+
475
+ # --------------------------
476
+ # Root (avoid 404 in Spaces)
477
+ # --------------------------
478
+
479
+ @app.get("/")
480
+ def root():
481
+ return {
482
+ "status": "ok",
483
+ "project": manifest_data.get("project"),
484
+ "version": manifest_data.get("version"),
485
+ "model": "RRFSavantMetaLogicV2",
486
+ "docs": "/docs",
487
+ "endpoints": ["/manifest", "/health", "/evaluate", "/predict", "/v1/rerank"],
488
+ }
489
+
490
+
491
+ # --------------------------
492
+ # Manifest (no naming clash)
493
+ # --------------------------
494
+
495
+ @app.get("/manifest")
496
+ def get_manifest():
497
+ return {
498
+ "model": "RRFSavantMetaLogicV2",
499
+ "version": manifest_data.get("version"),
500
+ "encoder": ENCODER_MODEL_ID,
501
+ "meta_logit": f"{META_LOGIT_REPO}/{META_LOGIT_FILENAME}",
502
+ "features": 15,
503
+ "phi_nodes": phi_nodes_public(),
504
+ "limits": {
505
+ "MAX_PROMPT_CHARS": MAX_PROMPT_CHARS,
506
+ "MAX_ANSWER_CHARS": MAX_ANSWER_CHARS,
507
+ "MAX_DOCS": MAX_DOCS,
508
+ "MAX_DOC_CHARS": MAX_DOC_CHARS,
509
+ }
510
+ }
511
+
512
+
513
+ @app.get("/health")
514
+ def health():
515
+ return {
516
+ "status": "ok",
517
+ "encoder_loaded": True,
518
+ "meta_logit_loaded": True,
519
+ "cnn_loaded": savant_cnn is not None,
520
+ "rrf_nodes_loaded": rrf_nodes is not None,
521
+ "manifest_version": manifest_data.get("version"),
522
+ "phi_nodes": len(PHI_NODES),
523
+ "device": str(device),
524
+ }
525
+
526
+
527
+ @app.post("/evaluate", response_model=EvaluateResponse)
528
+ def evaluate(req: EvaluateRequest):
529
+ try:
530
+ scores = compute_scores(req.prompt, req.answer)
531
+ return EvaluateResponse(scores=scores, manifest_version=str(manifest_data.get("version")))
532
+ except HTTPException:
533
+ raise
534
+ except Exception as e:
535
+ print(f"[Evaluate] Error: {e}", flush=True)
536
+ raise HTTPException(status_code=500, detail="Evaluation failed")
537
+
538
+
539
+ @app.post("/predict", response_model=PredictResponse)
540
+ def predict(req: PredictRequest):
541
+ try:
542
+ x = np.array([req.features], dtype=float)
543
+ p_good = float(meta_logit.predict_proba(x)[0][1])
544
+ return PredictResponse(p_good=p_good)
545
+ except Exception as e:
546
+ print(f"[Predict] Error: {e}", flush=True)
547
+ raise HTTPException(status_code=500, detail="Predict failed")
548
+
549
+
550
+ @app.post("/v1/rerank", response_model=RerankResponse)
551
+ def rerank(req: RerankRequest):
552
+ try:
553
+ if not req.query or not req.query.strip():
554
+ raise HTTPException(status_code=400, detail="query is empty")
555
+
556
+ if len(req.documents) > MAX_DOCS:
557
+ raise HTTPException(status_code=413, detail="Too many documents")
558
+
559
+ for d in req.documents:
560
+ if len(d) > MAX_DOC_CHARS:
561
+ raise HTTPException(status_code=413, detail="Document too large")
562
+
563
+ texts = [req.query] + req.documents
564
+ embs = agirrf_core.embedder.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
565
+
566
+ q_emb = embs[0]
567
+ d_embs = embs[1:]
568
+ scores = (d_embs @ q_emb).astype(float).tolist()
569
+
570
+ results = [{"id": i, "score": float(s)} for i, s in enumerate(scores)]
571
+ results.sort(key=lambda x: x["score"], reverse=True)
572
+
573
+ ranked = [RerankDocument(id=r["id"], score=r["score"], rank=i + 1) for i, r in enumerate(results)]
574
+ return RerankResponse(model_id=ENCODER_MODEL_ID, results=ranked)
575
+
576
+ except HTTPException:
577
+ raise
578
+ except Exception as e:
579
+ print(f"[Rerank] Error: {e}", flush=True)
580
+ raise HTTPException(status_code=500, detail="Rerank failed")