Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -88,14 +88,44 @@ OPENALEX_MAILTO = os.environ.get("OPENALEX_MAILTO", "rairo@sozofix.tech")
|
|
| 88 |
# 2. HELPER FUNCTIONS & AUTHENTICATION
|
| 89 |
# -----------------------------------------------------------------------------
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def _strip_json_fences(s: str) -> str:
|
| 92 |
-
"""Removes markdown code fences and cleans up AI string responses."""
|
| 93 |
s = (s or "").strip()
|
| 94 |
if "```" in s:
|
| 95 |
m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.DOTALL)
|
| 96 |
if m: return m.group(1).strip()
|
| 97 |
return s
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def verify_token(auth_header):
|
| 100 |
"""Verifies Firebase ID token from the Authorization header."""
|
| 101 |
if not auth_header or not auth_header.startswith('Bearer '):
|
|
@@ -453,84 +483,108 @@ def validate_odysseus_v6(t: dict) -> (bool, str):
|
|
| 453 |
@app.route('/api/trial/generate', methods=['POST'])
|
| 454 |
def generate_trial():
|
| 455 |
"""
|
| 456 |
-
Odysseus v10: The
|
| 457 |
-
|
| 458 |
Cost: 1 Spark.
|
| 459 |
"""
|
| 460 |
-
logger.info(">>> ODYSSEUS V10:
|
| 461 |
uid = verify_token(request.headers.get('Authorization'))
|
| 462 |
if not uid: return jsonify({"error": "Unauthorized"}), 401
|
| 463 |
-
|
| 464 |
-
payload = request.get_json()
|
| 465 |
ep_id, layer_key, subject = payload.get("epiphanyId"), payload.get("layerKey"), payload.get("subject")
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
trial_path = f"epiphanies/{ep_id}/trials/{layer_key}"
|
| 468 |
existing = db_ref.child(trial_path).get()
|
| 469 |
if existing: return jsonify(existing), 200
|
| 470 |
|
|
|
|
| 471 |
lock_ref = db_ref.child(f"epiphanies/{ep_id}/trialLocks/{layer_key}")
|
| 472 |
-
if lock_ref.get(): return jsonify({"
|
| 473 |
lock_ref.set({"uid": uid, "at": datetime.utcnow().isoformat()})
|
| 474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
try:
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
5. NO WRAPPERS: Return only the component code. Use React hooks.
|
| 497 |
-
|
| 498 |
-
JSON SCHEMA:
|
| 499 |
-
{{
|
| 500 |
-
"engine": "odysseus_v10",
|
| 501 |
-
"instrument_name": "Unique Title",
|
| 502 |
-
"mission_brief": "Feynman-style objective.",
|
| 503 |
-
"component_code": "const Instrument = ({{ onAction, onWin }}) => {{ ... }}; export default Instrument;",
|
| 504 |
-
"feynman_truth": "The law proven by this instrument."
|
| 505 |
-
}}
|
| 506 |
-
"""
|
| 507 |
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
)
|
| 513 |
-
|
| 514 |
-
trial_data = json.loads(_strip_json_fences(res.text))
|
| 515 |
-
|
| 516 |
user_ref = db_ref.child(f'users/{uid}')
|
| 517 |
-
|
| 518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
lock_ref.delete()
|
| 520 |
return jsonify({"error": "Insufficient Sparks"}), 402
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
lock_ref.delete()
|
| 527 |
-
|
| 528 |
-
|
|
|
|
| 529 |
|
| 530 |
except Exception as e:
|
| 531 |
lock_ref.delete()
|
| 532 |
-
logger.error(f"
|
| 533 |
-
return jsonify({"error":
|
| 534 |
|
| 535 |
# -----------------------------------------------------------------------------
|
| 536 |
# 5. THE CHIRON MENTOR & SYSTEM UTILS
|
|
|
|
| 88 |
# 2. HELPER FUNCTIONS & AUTHENTICATION
|
| 89 |
# -----------------------------------------------------------------------------
|
| 90 |
|
| 91 |
+
# -----------------------------------------------------------------------------
|
| 92 |
+
# ODYSSEUS V10 CONSTANTS & LINTING
|
| 93 |
+
# -----------------------------------------------------------------------------
|
| 94 |
+
FORBIDDEN_CODE_PATTERNS = [
|
| 95 |
+
r"\bfetch\s*\(", r"\bXMLHttpRequest\b", r"\bWebSocket\b", r"\blocalStorage\b",
|
| 96 |
+
r"\bsessionStorage\b", r"\bdocument\.cookie\b", r"\bwindow\.location\b",
|
| 97 |
+
r"\beval\s*\(", r"\bnew\s+Function\s*\(", r"\bimport\s*\(", r"\brequire\s*\("
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
REQUIRED_CODE_PATTERNS = [r"\bexport\s+default\s+Instrument\b"]
|
| 101 |
+
ALLOWED_INTERACTIONS = {"drag", "dial", "toggle", "tap", "hold", "keys"}
|
| 102 |
+
ALLOWED_WIN_TYPES = {"zone_dwell", "threshold", "sequence", "match"}
|
| 103 |
+
|
| 104 |
def _strip_json_fences(s: str) -> str:
|
|
|
|
| 105 |
s = (s or "").strip()
|
| 106 |
if "```" in s:
|
| 107 |
m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.DOTALL)
|
| 108 |
if m: return m.group(1).strip()
|
| 109 |
return s
|
| 110 |
|
| 111 |
+
def validate_manifest(m: dict) -> (bool, str):
|
| 112 |
+
if not isinstance(m, dict): return False, "Manifest is not an object"
|
| 113 |
+
for k in ("engine", "interaction", "win_rule"):
|
| 114 |
+
if k not in m: return False, f"Manifest missing '{k}'"
|
| 115 |
+
return True, "ok"
|
| 116 |
+
|
| 117 |
+
def lint_component_code(code: str) -> (bool, list):
|
| 118 |
+
errors = []
|
| 119 |
+
if not isinstance(code, str) or len(code.strip()) < 50:
|
| 120 |
+
return False, ["component_code missing/too short"]
|
| 121 |
+
for pat in FORBIDDEN_CODE_PATTERNS:
|
| 122 |
+
if re.search(pat, code): errors.append(f"forbidden_pattern")
|
| 123 |
+
for pat in REQUIRED_CODE_PATTERNS:
|
| 124 |
+
if not re.search(pat, code): errors.append(f"missing_export_default_Instrument")
|
| 125 |
+
if "onAction" not in code: errors.append("missing_call:onAction")
|
| 126 |
+
if "onWin" not in code: errors.append("missing_call:onWin")
|
| 127 |
+
return len(errors) == 0, errors
|
| 128 |
+
|
| 129 |
def verify_token(auth_header):
|
| 130 |
"""Verifies Firebase ID token from the Authorization header."""
|
| 131 |
if not auth_header or not auth_header.startswith('Bearer '):
|
|
|
|
| 483 |
@app.route('/api/trial/generate', methods=['POST'])
|
| 484 |
def generate_trial():
|
| 485 |
"""
|
| 486 |
+
Odysseus v10: The Self-Repairing Instrumentalist.
|
| 487 |
+
Generates bespoke Vanilla React components with a 3-pass repair loop.
|
| 488 |
Cost: 1 Spark.
|
| 489 |
"""
|
| 490 |
+
logger.info(">>> ODYSSEUS V10: GENERATING TRIAL")
|
| 491 |
uid = verify_token(request.headers.get('Authorization'))
|
| 492 |
if not uid: return jsonify({"error": "Unauthorized"}), 401
|
| 493 |
+
|
| 494 |
+
payload = request.get_json(silent=True) or {}
|
| 495 |
ep_id, layer_key, subject = payload.get("epiphanyId"), payload.get("layerKey"), payload.get("subject")
|
| 496 |
|
| 497 |
+
if not all([ep_id, layer_key, subject]):
|
| 498 |
+
return jsonify({"error": "Missing context parameters."}), 400
|
| 499 |
+
|
| 500 |
+
# 1. Cache Check
|
| 501 |
trial_path = f"epiphanies/{ep_id}/trials/{layer_key}"
|
| 502 |
existing = db_ref.child(trial_path).get()
|
| 503 |
if existing: return jsonify(existing), 200
|
| 504 |
|
| 505 |
+
# 2. Concurrency Lock
|
| 506 |
lock_ref = db_ref.child(f"epiphanies/{ep_id}/trialLocks/{layer_key}")
|
| 507 |
+
if lock_ref.get(): return jsonify({"status": "locked"}), 409
|
| 508 |
lock_ref.set({"uid": uid, "at": datetime.utcnow().isoformat()})
|
| 509 |
|
| 510 |
+
# 3. Context Retrieval
|
| 511 |
+
layer_obj = db_ref.child(f"epiphanies/{ep_id}/layers/{layer_key}").get() or {}
|
| 512 |
+
ctx_text = layer_obj.get("text", "General scientific principles.")
|
| 513 |
+
|
| 514 |
+
# 4. Forge & Repair Loop
|
| 515 |
+
attempts, forged, last_raw = 0, None, ""
|
| 516 |
+
|
| 517 |
+
base_prompt = f"""
|
| 518 |
+
You are Athena's Master Instrumentalist. Forge a tactile scientific Instrument for {subject}.
|
| 519 |
+
Layer Context: {ctx_text}
|
| 520 |
+
|
| 521 |
+
TASK: Write a React component named 'Instrument'.
|
| 522 |
+
REQUIREMENTS:
|
| 523 |
+
1. Vanilla React ONLY. No external libraries (No Matter.js). Use SVG/Canvas for visuals.
|
| 524 |
+
2. Tactile: The system must fail/decay if user does nothing.
|
| 525 |
+
3. Handshake: Must accept and call `props.onAction()` (on touch) and `props.onWin()` (on success).
|
| 526 |
+
4. Design: Midnight Navy #0B1120, Gold #D4AF37.
|
| 527 |
+
|
| 528 |
+
JSON SCHEMA:
|
| 529 |
+
{{
|
| 530 |
+
"engine": "odysseus_v10",
|
| 531 |
+
"instrument_name": "Title",
|
| 532 |
+
"mission_brief": "Goal",
|
| 533 |
+
"instrument_manifest": {{ "engine": "odysseus_v10", "interaction": "drag|tap|hold", "win_rule": {{ "type": "zone_dwell" }} }},
|
| 534 |
+
"component_code": "const Instrument = (props) => {{ ... }}; export default Instrument;",
|
| 535 |
+
"feynman_truth": "The law proven."
|
| 536 |
+
}}
|
| 537 |
+
"""
|
| 538 |
+
|
| 539 |
try:
|
| 540 |
+
while attempts < 3 and not forged:
|
| 541 |
+
attempts += 1
|
| 542 |
+
current_prompt = base_prompt if attempts == 1 else f"REPAIR THIS CODE: {last_raw}\nERRORS: {last_errs}"
|
| 543 |
+
|
| 544 |
+
res = client.models.generate_content(
|
| 545 |
+
model=ATHENA_FLASH,
|
| 546 |
+
contents=current_prompt,
|
| 547 |
+
config=types.GenerateContentConfig(response_mime_type="application/json")
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
last_raw = _strip_json_fences(res.text)
|
| 551 |
+
candidate = json.loads(last_raw)
|
| 552 |
+
|
| 553 |
+
# Validation logic
|
| 554 |
+
ok_m, _ = validate_manifest(candidate.get("instrument_manifest"))
|
| 555 |
+
ok_c, last_errs = lint_component_code(candidate.get("component_code"))
|
| 556 |
+
|
| 557 |
+
if ok_m and ok_c:
|
| 558 |
+
forged = candidate
|
| 559 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
+
if not forged:
|
| 562 |
+
lock_ref.delete()
|
| 563 |
+
return jsonify({"status": "failed", "reason": "Compilation failure"}), 422
|
| 564 |
+
|
| 565 |
+
# 5. Atomic Credit Deduction (1 Spark)
|
|
|
|
|
|
|
|
|
|
| 566 |
user_ref = db_ref.child(f'users/{uid}')
|
| 567 |
+
def credits_txn(cur):
|
| 568 |
+
cur = cur or 0
|
| 569 |
+
return cur - 1 if cur >= 1 else cur
|
| 570 |
+
|
| 571 |
+
result = user_ref.child('credits').transaction(credits_txn)
|
| 572 |
+
if result < 0:
|
| 573 |
lock_ref.delete()
|
| 574 |
return jsonify({"error": "Insufficient Sparks"}), 402
|
| 575 |
+
|
| 576 |
+
# 6. Finalize
|
| 577 |
+
forged["createdAt"] = datetime.utcnow().isoformat()
|
| 578 |
+
db_ref.child(trial_path).set(forged)
|
|
|
|
| 579 |
lock_ref.delete()
|
| 580 |
+
|
| 581 |
+
logger.info(f"Odysseus v10 forged for {subject} in {attempts} attempts.")
|
| 582 |
+
return jsonify(forged), 201
|
| 583 |
|
| 584 |
except Exception as e:
|
| 585 |
lock_ref.delete()
|
| 586 |
+
logger.error(f"Forging Global Error: {e}")
|
| 587 |
+
return jsonify({"error": "The laws of physics failed to compile."}), 500
|
| 588 |
|
| 589 |
# -----------------------------------------------------------------------------
|
| 590 |
# 5. THE CHIRON MENTOR & SYSTEM UTILS
|