antonypamo commited on
Commit
e027e92
·
verified ·
1 Parent(s): 4b2d865

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -72
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # ======================================================
2
- # Savant RRF Φ12.0 — app.py (AGIRRFCore-aligned)
3
  # Uses the same AGIRRFCore logic as RRFSavant_AGI_Core_Colab
4
  # ======================================================
5
 
@@ -22,6 +22,16 @@ from huggingface_hub import hf_hub_download
22
  import joblib
23
 
24
 
 
 
 
 
 
 
 
 
 
 
25
  # ======================================================
26
  # 1) MANIFEST
27
  # ======================================================
@@ -35,27 +45,27 @@ DEFAULT_MANIFEST = {
35
 
36
  MANIFEST_PATH = Path(__file__).parent / "savant_rrf_api_manifest_phi12.json"
37
 
38
- def load_manifest() -> Dict[str, Any]:
39
  if MANIFEST_PATH.exists():
40
  try:
41
  print(f"[Manifest] Loading from {MANIFEST_PATH}", flush=True)
42
  return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
43
  except Exception as e:
44
  print(f"[Manifest] Invalid JSON: {e}", flush=True)
45
-
46
  print("[Manifest] Using DEFAULT_MANIFEST", flush=True)
47
  return DEFAULT_MANIFEST
48
 
49
- manifest = load_manifest()
50
- print("[Manifest] version:", manifest.get("version"), flush=True)
51
 
52
 
53
  # ======================================================
54
  # 2) Global config
55
  # ======================================================
56
 
57
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
58
- os.environ["HF_TOKEN"] = HF_TOKEN
 
59
 
60
  ENCODER_MODEL_ID = "antonypamo/RRFSAVANTMADE"
61
  META_LOGIT_REPO = "antonypamo/RRFSavantMetaLogicV2"
@@ -63,29 +73,57 @@ META_LOGIT_FILENAME = "logreg_rrf_savant.joblib"
63
 
64
  RRF_DATASET_REPO = "antonypamo/savant_rrf1_curated"
65
 
66
-
67
- def hf_data_path(filename: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  try:
69
  return hf_hub_download(
70
- repo_id=RRF_DATASET_REPO,
71
  filename=filename,
72
- repo_type="dataset",
73
- token=HF_TOKEN or None,
74
  )
75
  except Exception as e:
76
- print(f"[Dataset] Missing {filename}: {e}", flush=True)
 
 
 
 
 
 
 
77
  return None
78
 
79
 
 
 
 
 
 
 
 
 
 
80
  # ======================================================
81
- # 3) Optional artifacts
82
  # ======================================================
83
 
84
- SAVANT_CNN_PATH = hf_data_path("savant_cnn.pt")
85
- RRF_NODES_PATH = hf_data_path("rrf_nodes.pt")
86
- RRF_TUTOR_JSONL = hf_data_path("rrf_tutor_curated.jsonl")
87
-
88
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
89
 
90
 
91
  # ======================================================
@@ -109,6 +147,7 @@ class SavantCNN(nn.Module):
109
  x = x.view(x.size(0), -1)
110
  return self.fc(x)
111
 
 
112
  savant_cnn = None
113
  if SAVANT_CNN_PATH:
114
  try:
@@ -125,11 +164,11 @@ if RRF_NODES_PATH:
125
  rrf_nodes = torch.load(RRF_NODES_PATH, map_location=device)
126
  print("✅ RRF nodes loaded", flush=True)
127
  except Exception as e:
128
- print(f"⚠️ RRF nodes failed: {e}", flush=True)
129
 
130
 
131
  # ======================================================
132
- # 5) Φ-node ontology (same spirit as notebook: 8 nodes -> one-hot 8)
133
  # ======================================================
134
 
135
  @dataclass
@@ -137,7 +176,7 @@ class PhiNode:
137
  name: str
138
  description: str
139
  tags: List[str] = field(default_factory=list)
140
- embedding: Optional[np.ndarray] = None
141
 
142
  PHI_NODES: List[PhiNode] = [
143
  PhiNode("Φ0_seed", "Genesis seed, core identity and origin.", ["genesis","identity","anchor"]),
@@ -152,8 +191,13 @@ PHI_NODES: List[PhiNode] = [
152
  PHI_NAME_TO_IDX = {n.name: i for i, n in enumerate(PHI_NODES)}
153
 
154
 
 
 
 
 
 
155
  # ======================================================
156
- # 6) CoherenceModel (fully implemented; notebook had '...' placeholders)
157
  # ======================================================
158
 
159
  class CoherenceModel:
@@ -186,7 +230,7 @@ coherence_model = CoherenceModel()
186
 
187
 
188
  # ======================================================
189
- # 7) AGIRRFCore (same logic as notebook; cleaned)
190
  # ======================================================
191
 
192
  class AGIRRFCore:
@@ -199,9 +243,7 @@ class AGIRRFCore:
199
  self.phi_nodes = phi_nodes
200
  self.coherence_model = coherence_model
201
 
202
- print(f"🔄 Loading sentence-transformer: {st_model_name} on {device} ...", flush=True)
203
- # SentenceTransformer expects device as string usually
204
- st_device = "cuda" if torch.cuda.is_available() else "cpu"
205
  self.embedder = SentenceTransformer(st_model_name, device=st_device)
206
  print("✅ Embedder loaded", flush=True)
207
 
@@ -228,9 +270,8 @@ class AGIRRFCore:
228
  return float(freqs[idx])
229
 
230
  def _phi_omega(self, energy: float, dom_freq: float) -> Tuple[float, float]:
231
- # same mapping as notebook
232
- phi = 1.0 - math.exp(-float(energy)) # saturating [0,1)
233
- omega = math.tanh(dom_freq * 10.0) # [0,1)
234
  return float(phi), float(omega)
235
 
236
  def _closest_phi_node(self, vec: np.ndarray) -> Tuple[str, float]:
@@ -238,8 +279,7 @@ class AGIRRFCore:
238
  return "unknown", 0.0
239
  v = np.asarray(vec, dtype=float).ravel()
240
  v_norm = np.linalg.norm(v) + 1e-9
241
- best_name = "unknown"
242
- best_cos = -1.0
243
  for node in self.phi_nodes:
244
  e = node.embedding
245
  if e is None:
@@ -253,9 +293,7 @@ class AGIRRFCore:
253
  def analyze(self, text: str, context_label: str = "query") -> Dict[str, Any]:
254
  vec = self._embed_text(text)
255
 
256
- # notebook energy = dot(vec, vec) (not normalized)
257
  energy = float(np.dot(vec, vec))
258
-
259
  dom_freq = self._dominant_frequency(vec)
260
  phi, omega = self._phi_omega(energy, dom_freq)
261
 
@@ -265,7 +303,6 @@ class AGIRRFCore:
265
  S_RRF, C_RRF = 0.0, 0.0
266
 
267
  coherence = 0.5 * float(S_RRF) + 0.5 * float(C_RRF)
268
-
269
  closest_name, closest_cos = self._closest_phi_node(vec)
270
 
271
  return {
@@ -291,80 +328,91 @@ agirrf_core = AGIRRFCore(
291
 
292
 
293
  # ======================================================
294
- # 8) Load Meta-Logit (15D pipeline)
295
  # ======================================================
296
 
297
  print("🔄 Loading meta-logit...", flush=True)
298
- meta_logit_path = hf_hub_download(
299
  repo_id=META_LOGIT_REPO,
300
  filename=META_LOGIT_FILENAME,
301
- token=HF_TOKEN or None,
302
  )
 
 
 
 
 
303
  meta_logit = joblib.load(meta_logit_path)
304
- print("✅ Meta-logit ready", flush=True)
 
 
 
 
305
 
306
 
307
  # ======================================================
308
- # 9) Feature mapping (exact notebook structure: 7 + one-hot 8 = 15)
309
  # ======================================================
310
 
311
  def rrf_state_to_features(state: Dict[str, Any]) -> np.ndarray:
312
- phi = float(state.get("phi", 0.0))
313
  omega = float(state.get("omega", 0.0))
314
- coh = float(state.get("coherence", 0.0))
315
  S_RRF = float(state.get("S_RRF", 0.0))
316
  C_RRF = float(state.get("C_RRF", 0.0))
317
- E_H = float(state.get("hamiltonian_energy", 0.0))
318
  dom_f = float(state.get("dominant_frequency", 0.0))
319
 
320
  phi_name = state.get("closest_phi_node", "unknown")
321
- n_phi = len(PHI_NODES)
322
- phi_onehot = np.zeros(n_phi, dtype=float)
323
  idx = PHI_NAME_TO_IDX.get(phi_name)
324
  if idx is not None:
325
  phi_onehot[idx] = 1.0
326
 
327
- base_feats = np.array([phi, omega, coh, S_RRF, C_RRF, E_H, dom_f], dtype=float)
328
- feats = np.concatenate([base_feats, phi_onehot], axis=0) # 15D
329
- return feats
330
 
331
 
332
  # ======================================================
333
- # 10) Core scoring for (prompt, answer)
334
- # We'll analyze combined QA text to stay consistent and stable.
335
  # ======================================================
336
 
337
- def get_embedding_normed(text: str) -> np.ndarray:
338
  return agirrf_core.embedder.encode([text], convert_to_numpy=True, normalize_embeddings=True)[0]
339
 
340
  def compute_scores(prompt: str, answer: str) -> Dict[str, Any]:
 
 
341
  if not prompt.strip() or not answer.strip():
342
  raise ValueError("Empty prompt/answer")
343
 
344
- # Classic cosine (extra signal, not part of 15D)
345
- e_p = get_embedding_normed(prompt)
346
- e_a = get_embedding_normed(answer)
 
 
 
347
  cosine = float(np.dot(e_p, e_a))
348
 
349
- # AGI-RRF state from combined text (stable single-state features)
350
  qa_text = f"Q: {prompt}\nA: {answer}"
351
  state = agirrf_core.analyze(qa_text, context_label="qa")
352
-
353
  feats = rrf_state_to_features(state).reshape(1, -1)
 
354
  p_good = float(meta_logit.predict_proba(feats)[0][1])
355
 
356
- # Keep your public metrics, but now grounded
357
  SRRF = p_good
358
  CRRF = p_good * cosine
359
  E_phi = 0.5 * (p_good + abs(cosine))
360
 
361
  return {
362
- "cosine": cosine,
363
  "p_good": p_good,
364
  "SRRF": SRRF,
365
  "CRRF": CRRF,
366
  "E_phi": E_phi,
367
- # expose state so it's debuggable (very important for Savant)
 
 
368
  "phi": float(state["phi"]),
369
  "omega": float(state["omega"]),
370
  "coherence": float(state["coherence"]),
@@ -382,16 +430,16 @@ def compute_scores(prompt: str, answer: str) -> Dict[str, Any]:
382
  # ======================================================
383
 
384
  class EvaluateRequest(BaseModel):
 
385
  prompt: str
386
  answer: str
387
- model_label: Optional[str] = None
388
 
389
  class EvaluateResponse(BaseModel):
390
  scores: Dict[str, Any]
391
  manifest_version: str
392
 
393
  class PredictRequest(BaseModel):
394
- # direct 15D call (matches your MetaLogit /predict pattern)
395
  features: List[float] = Field(..., min_length=15, max_length=15)
396
 
397
  class PredictResponse(BaseModel):
@@ -400,7 +448,7 @@ class PredictResponse(BaseModel):
400
  class RerankRequest(BaseModel):
401
  query: str
402
  documents: List[str]
403
- alpha: float = 0.2
404
 
405
  class RerankDocument(BaseModel):
406
  id: int
@@ -419,42 +467,75 @@ class RerankResponse(BaseModel):
419
 
420
  app = FastAPI(
421
  title="Savant RRF Φ12.0 API",
422
- version="1.2.0",
423
  description="AGIRRFCore-aligned Meta-Logic, Reranking & Quality Evaluation",
424
  )
425
 
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  @app.get("/manifest")
428
- def manifest():
429
  return {
430
  "model": "RRFSavantMetaLogicV2",
431
- "version": "Φ12.0",
432
  "encoder": ENCODER_MODEL_ID,
 
433
  "features": 15,
434
- "phi_nodes": PHI_NODES,
 
 
 
 
 
 
435
  }
436
 
437
 
438
  @app.get("/health")
439
  def health():
440
  return {
441
- "encoder": True,
442
- "meta_logit": True,
 
443
  "cnn_loaded": savant_cnn is not None,
444
  "rrf_nodes_loaded": rrf_nodes is not None,
445
- "manifest": manifest.get("version"),
446
  "phi_nodes": len(PHI_NODES),
 
447
  }
448
 
 
449
  @app.post("/evaluate", response_model=EvaluateResponse)
450
  def evaluate(req: EvaluateRequest):
451
  try:
452
  scores = compute_scores(req.prompt, req.answer)
453
- return EvaluateResponse(scores=scores, manifest_version=manifest.get("version"))
 
 
454
  except Exception as e:
455
  print(f"[Evaluate] Error: {e}", flush=True)
456
  raise HTTPException(status_code=500, detail="Evaluation failed")
457
 
 
458
  @app.post("/predict", response_model=PredictResponse)
459
  def predict(req: PredictRequest):
460
  try:
@@ -465,20 +546,35 @@ def predict(req: PredictRequest):
465
  print(f"[Predict] Error: {e}", flush=True)
466
  raise HTTPException(status_code=500, detail="Predict failed")
467
 
 
468
  @app.post("/v1/rerank", response_model=RerankResponse)
469
  def rerank(req: RerankRequest):
470
  try:
 
 
 
 
 
 
 
 
 
 
471
  texts = [req.query] + req.documents
472
  embs = agirrf_core.embedder.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
 
473
  q_emb = embs[0]
474
  d_embs = embs[1:]
475
- scores = (d_embs @ q_emb).tolist()
476
 
477
  results = [{"id": i, "score": float(s)} for i, s in enumerate(scores)]
478
  results.sort(key=lambda x: x["score"], reverse=True)
479
 
480
- ranked = [RerankDocument(id=r["id"], score=r["score"], rank=i+1) for i, r in enumerate(results)]
481
  return RerankResponse(model_id=ENCODER_MODEL_ID, results=ranked)
 
 
 
482
  except Exception as e:
483
  print(f"[Rerank] Error: {e}", flush=True)
484
  raise HTTPException(status_code=500, detail="Rerank failed")
 
1
  # ======================================================
2
+ # Savant RRF Φ12.0 — app.py (AGIRRFCore-aligned, HARDENED)
3
  # Uses the same AGIRRFCore logic as RRFSavant_AGI_Core_Colab
4
  # ======================================================
5
 
 
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
  # ======================================================
 
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"
 
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
  # ======================================================
 
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:
 
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
 
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"]),
 
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:
 
230
 
231
 
232
  # ======================================================
233
+ # 7) AGIRRFCore (aligned)
234
  # ======================================================
235
 
236
  class AGIRRFCore:
 
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
 
 
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]:
 
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:
 
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
 
 
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 {
 
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"]),
 
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):
 
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
 
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:
 
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")