Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -423,339 +423,117 @@ def deep_dive():
|
|
| 423 |
|
| 424 |
|
| 425 |
# -----------------------------------------------------------------------------
|
| 426 |
-
# ODYSSEUS
|
| 427 |
-
# - Generates a Trial once per (epiphanyId, layerKey) and persists it forever
|
| 428 |
-
# - Cache hit returns existing Trial (no re-charge)
|
| 429 |
-
# - If Gemini output can't "compile" into schema: return 422 + store diagnostics
|
| 430 |
-
# - Credits are deducted ONLY after schema validation passes
|
| 431 |
-
# - Uses an RTDB lock (transaction) to avoid double-click / concurrent generation
|
| 432 |
# -----------------------------------------------------------------------------
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
return
|
| 447 |
-
|
| 448 |
-
def validate_trial_schema(t: dict) -> (bool, str):
|
| 449 |
-
"""
|
| 450 |
-
Minimal v1 compiler checks (deterministic frontend assumptions):
|
| 451 |
-
- renderer must be in fixed set (so UI knows which canvas renderer to use)
|
| 452 |
-
- grammar must be one of three
|
| 453 |
-
- 1 variable + 1 slider control bound to that variable
|
| 454 |
-
- win zone + dwell
|
| 455 |
-
"""
|
| 456 |
-
if not isinstance(t, dict):
|
| 457 |
-
return False, "trial is not an object"
|
| 458 |
-
|
| 459 |
-
engine = t.get("engine")
|
| 460 |
-
if not isinstance(engine, dict):
|
| 461 |
-
return False, "engine missing/invalid"
|
| 462 |
-
|
| 463 |
-
renderer = t.get("renderer")
|
| 464 |
-
grammar = t.get("grammar")
|
| 465 |
-
labels = t.get("labels")
|
| 466 |
-
logic = t.get("logic")
|
| 467 |
-
|
| 468 |
-
if renderer not in TRIAL_RENDERERS:
|
| 469 |
-
return False, f"renderer must be one of {sorted(TRIAL_RENDERERS)} (got {renderer})"
|
| 470 |
-
if grammar not in TRIAL_GRAMMARS:
|
| 471 |
-
return False, f"grammar must be one of {sorted(TRIAL_GRAMMARS)} (got {grammar})"
|
| 472 |
-
|
| 473 |
-
if not isinstance(labels, dict):
|
| 474 |
-
return False, "labels missing/invalid"
|
| 475 |
-
if not isinstance(labels.get("title"), str) or len(labels["title"].strip()) < 2:
|
| 476 |
-
return False, "labels.title missing/invalid"
|
| 477 |
-
if not isinstance(labels.get("goal"), str) or len(labels["goal"].strip()) < 3:
|
| 478 |
-
return False, "labels.goal missing/invalid"
|
| 479 |
-
if not isinstance(labels.get("controls"), list) or len(labels["controls"]) < 1:
|
| 480 |
-
return False, "labels.controls missing/invalid"
|
| 481 |
-
|
| 482 |
-
if not isinstance(logic, dict):
|
| 483 |
-
return False, "logic missing/invalid"
|
| 484 |
-
|
| 485 |
-
variables = logic.get("variables")
|
| 486 |
-
controls = logic.get("controls")
|
| 487 |
-
win = logic.get("win")
|
| 488 |
-
fail = logic.get("fail")
|
| 489 |
-
|
| 490 |
-
if not isinstance(variables, list) or len(variables) != 1:
|
| 491 |
-
return False, "logic.variables must be a list of exactly 1 variable for v1"
|
| 492 |
-
if not isinstance(controls, list) or len(controls) != 1:
|
| 493 |
-
return False, "logic.controls must be a list of exactly 1 control for v1"
|
| 494 |
-
if not isinstance(win, dict):
|
| 495 |
-
return False, "logic.win missing/invalid"
|
| 496 |
-
if not isinstance(fail, dict):
|
| 497 |
-
return False, "logic.fail missing/invalid"
|
| 498 |
-
|
| 499 |
-
v = variables[0]
|
| 500 |
-
if not isinstance(v, dict):
|
| 501 |
-
return False, "variable[0] invalid"
|
| 502 |
-
for k in ("id", "label", "min", "max", "start"):
|
| 503 |
-
if k not in v:
|
| 504 |
-
return False, f"variable[0].{k} missing"
|
| 505 |
-
if not isinstance(v["id"], str) or not v["id"].strip():
|
| 506 |
-
return False, "variable[0].id invalid"
|
| 507 |
-
if not isinstance(v["label"], str) or len(v["label"].strip()) < 2:
|
| 508 |
-
return False, "variable[0].label invalid"
|
| 509 |
-
if not _is_num(v["min"]) or not _is_num(v["max"]) or not _is_num(v["start"]):
|
| 510 |
-
return False, "variable[0] numeric fields invalid"
|
| 511 |
-
if v["min"] >= v["max"]:
|
| 512 |
-
return False, "variable[0] min>=max"
|
| 513 |
-
if not (v["min"] <= v["start"] <= v["max"]):
|
| 514 |
-
return False, "variable[0] start out of range"
|
| 515 |
-
|
| 516 |
-
c = controls[0]
|
| 517 |
-
if not isinstance(c, dict):
|
| 518 |
-
return False, "control[0] invalid"
|
| 519 |
-
for k in ("id", "label", "type", "min", "max", "step", "binds_to", "gain"):
|
| 520 |
-
if k not in c:
|
| 521 |
-
return False, f"control[0].{k} missing"
|
| 522 |
-
if c.get("type") != "slider":
|
| 523 |
-
return False, "control[0].type must be 'slider' for v1"
|
| 524 |
-
if not isinstance(c["label"], str) or len(c["label"].strip()) < 2:
|
| 525 |
-
return False, "control[0].label invalid"
|
| 526 |
-
if not _is_num(c["min"]) or not _is_num(c["max"]) or not _is_num(c["step"]) or not _is_num(c["gain"]):
|
| 527 |
-
return False, "control[0] numeric fields invalid"
|
| 528 |
-
if c["min"] >= c["max"]:
|
| 529 |
-
return False, "control[0] min>=max"
|
| 530 |
-
if c.get("binds_to") != v["id"]:
|
| 531 |
-
return False, "control[0].binds_to must match variable id"
|
| 532 |
-
|
| 533 |
-
if win.get("type") != "zone":
|
| 534 |
-
return False, "win.type must be 'zone' for v1"
|
| 535 |
-
if win.get("var") != v["id"]:
|
| 536 |
-
return False, "win.var must match variable id"
|
| 537 |
-
if not _is_num(win.get("min")) or not _is_num(win.get("max")):
|
| 538 |
-
return False, "win.min/win.max invalid"
|
| 539 |
-
if win["min"] >= win["max"]:
|
| 540 |
-
return False, "win min>=max"
|
| 541 |
-
dwell = win.get("dwell_ms", 0)
|
| 542 |
-
if not _is_num(dwell) or dwell < 0:
|
| 543 |
-
return False, "win.dwell_ms invalid"
|
| 544 |
-
|
| 545 |
-
if not isinstance(fail.get("reason"), str) or len(fail["reason"].strip()) < 3:
|
| 546 |
-
return False, "fail.reason missing/invalid"
|
| 547 |
-
|
| 548 |
-
return True, "ok"
|
| 549 |
-
|
| 550 |
|
| 551 |
@app.route('/api/trial/generate', methods=['POST'])
|
| 552 |
def generate_trial():
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
|
|
|
|
|
|
| 556 |
uid = verify_token(request.headers.get('Authorization'))
|
| 557 |
-
if not uid:
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
layer_key = payload.get("layerKey") # genesis | scientific_core | engineering_edge | cross_pollination
|
| 563 |
subject = payload.get("subject")
|
| 564 |
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
# 3) CACHE FOREVER
|
| 575 |
-
trial_path = f"epiphanies/{epiphany_id}/trials/{layer_key}"
|
| 576 |
-
trial_ref = db_ref.child(trial_path)
|
| 577 |
-
existing = trial_ref.get()
|
| 578 |
-
if existing:
|
| 579 |
-
return jsonify(existing), 200
|
| 580 |
-
|
| 581 |
-
# 4) LOCK (atomic via transaction)
|
| 582 |
-
lock_id = str(uuid.uuid4())
|
| 583 |
-
lock_path = f"epiphanies/{epiphany_id}/trialLocks/{layer_key}"
|
| 584 |
-
lock_ref = db.reference(lock_path)
|
| 585 |
-
|
| 586 |
-
lock_payload = {"uid": uid, "lockId": lock_id, "at": datetime.utcnow().isoformat()}
|
| 587 |
-
|
| 588 |
-
def lock_txn(cur):
|
| 589 |
-
# If a lock already exists, keep it (do not overwrite)
|
| 590 |
-
if cur:
|
| 591 |
-
return cur
|
| 592 |
-
return lock_payload
|
| 593 |
-
|
| 594 |
-
lock_result = lock_ref.transaction(lock_txn)
|
| 595 |
-
# If someone else already holds the lock, return 409
|
| 596 |
-
if lock_result and lock_result.get("lockId") != lock_id:
|
| 597 |
-
return jsonify({
|
| 598 |
-
"status": "locked",
|
| 599 |
-
"error": "Trial is being generated. Please retry shortly.",
|
| 600 |
-
"cooldown_ms": 1200
|
| 601 |
-
}), 409
|
| 602 |
-
|
| 603 |
-
def cleanup_lock_best_effort():
|
| 604 |
-
try:
|
| 605 |
-
cur = lock_ref.get()
|
| 606 |
-
if cur and cur.get("lockId") == lock_id:
|
| 607 |
-
lock_ref.delete()
|
| 608 |
-
except Exception:
|
| 609 |
-
pass
|
| 610 |
-
|
| 611 |
-
# 5) GEMINI GENERATION (2 attempts), NO FALLBACK
|
| 612 |
-
model_name = ATHENA_FLASH
|
| 613 |
-
attempts = 0
|
| 614 |
-
raw_outputs = []
|
| 615 |
-
last_validation_error = None
|
| 616 |
-
last_exception = None
|
| 617 |
-
|
| 618 |
-
trial_prompt = f"""
|
| 619 |
-
You are designing a short, playable scientific Trial for: {subject}
|
| 620 |
-
Layer: {layer_key}
|
| 621 |
-
|
| 622 |
-
Output JSON ONLY (no markdown).
|
| 623 |
-
|
| 624 |
-
Rules:
|
| 625 |
-
- Choose a FREE domain_label (any string) describing the concept (e.g., "orbital mechanics", "distillation").
|
| 626 |
-
- Choose renderer from EXACTLY: thermal, mechanical, electrical, chemical, biological
|
| 627 |
-
- Choose grammar from EXACTLY: accumulate, balance, transform
|
| 628 |
-
- Must be solvable in <= 30 seconds.
|
| 629 |
-
- v1 STRICT: 1 variable and 1 slider control.
|
| 630 |
-
|
| 631 |
-
JSON SCHEMA:
|
| 632 |
-
{{
|
| 633 |
-
"engine": {{"name":"odysseus","version":"v1"}},
|
| 634 |
-
"renderer":"thermal|mechanical|electrical|chemical|biological",
|
| 635 |
-
"domain_label":"free text",
|
| 636 |
-
"grammar":"accumulate|balance|transform",
|
| 637 |
-
"labels": {{
|
| 638 |
-
"title":"Short Title",
|
| 639 |
-
"goal":"One-line goal",
|
| 640 |
-
"controls":["Single control label"]
|
| 641 |
-
}},
|
| 642 |
-
"logic": {{
|
| 643 |
-
"variables":[{{"id":"x","label":"Label (include units)","min":number,"max":number,"start":number}}],
|
| 644 |
-
"controls":[{{"id":"c1","label":"Same as labels.controls[0]","type":"slider","min":number,"max":number,"step":number,"binds_to":"x","gain":number}}],
|
| 645 |
-
"win":{{"type":"zone","var":"x","min":number,"max":number,"dwell_ms":number}},
|
| 646 |
-
"fail":{{"type":"out_of_range","reason":"Scientific reason for instability"}},
|
| 647 |
-
"difficulty": number
|
| 648 |
-
}},
|
| 649 |
-
"narrative": {{
|
| 650 |
-
"on_win":"One sentence insight.",
|
| 651 |
-
"on_fail":"One sentence failure explanation."
|
| 652 |
-
}}
|
| 653 |
-
}}
|
| 654 |
-
"""
|
| 655 |
|
| 656 |
try:
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
"
|
| 687 |
-
"
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
"attempts": attempts,
|
| 692 |
-
"validation_error": last_validation_error,
|
| 693 |
-
"exception": last_exception,
|
| 694 |
-
"raw_output_preview": raw_outputs[-1] if raw_outputs else None,
|
| 695 |
-
"at": datetime.utcnow().isoformat()
|
| 696 |
-
}
|
| 697 |
-
db_ref.child(f"trial_failures/{uid}/{epiphany_id}/{layer_key}").push(failure_doc)
|
| 698 |
-
|
| 699 |
-
cleanup_lock_best_effort()
|
| 700 |
-
return jsonify({
|
| 701 |
-
"status": "failed",
|
| 702 |
-
"epiphanyId": epiphany_id,
|
| 703 |
-
"layerKey": layer_key,
|
| 704 |
-
"subject": subject,
|
| 705 |
-
"reason": "schema_invalid" if last_validation_error else "model_error",
|
| 706 |
-
"details": {
|
| 707 |
-
"validation_error": last_validation_error,
|
| 708 |
-
"exception": last_exception,
|
| 709 |
-
"attempts": attempts,
|
| 710 |
-
"raw_output_preview": raw_outputs[-1] if raw_outputs else None
|
| 711 |
-
},
|
| 712 |
-
"next": {"action": "regenerate", "cooldown_ms": 1200}
|
| 713 |
-
}), 422
|
| 714 |
-
|
| 715 |
-
# 7) CHARGE (atomic) ONLY AFTER VALID TRIAL EXISTS
|
| 716 |
-
credits_ref = db.reference(f"users/{uid}/credits")
|
| 717 |
-
did_deduct = {"ok": False}
|
| 718 |
-
|
| 719 |
-
def credits_txn(cur):
|
| 720 |
-
cur = cur or 0
|
| 721 |
-
if cur < 1:
|
| 722 |
-
return cur
|
| 723 |
-
did_deduct["ok"] = True
|
| 724 |
-
return cur - 1
|
| 725 |
-
|
| 726 |
-
credits_ref.transaction(credits_txn)
|
| 727 |
-
|
| 728 |
-
if not did_deduct["ok"]:
|
| 729 |
-
cleanup_lock_best_effort()
|
| 730 |
-
return jsonify({"error": "Insufficient Sparks. (Trial requires 1 spark)"}), 402
|
| 731 |
-
|
| 732 |
-
# 8) PERSIST TRIAL FOREVER
|
| 733 |
-
trial_ref.set(trial_json)
|
| 734 |
-
|
| 735 |
-
cleanup_lock_best_effort()
|
| 736 |
-
logger.info(f"Trial manifested & persisted: {trial_path}")
|
| 737 |
-
return jsonify(trial_json), 201
|
| 738 |
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
"
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
"
|
| 752 |
-
"at": datetime.utcnow().isoformat()
|
| 753 |
-
}
|
| 754 |
-
db_ref.child(f"trial_failures/{uid}/{epiphany_id}/{layer_key}").push(failure_doc)
|
| 755 |
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
|
| 760 |
# -----------------------------------------------------------------------------
|
| 761 |
# 5. THE CHIRON MENTOR & SYSTEM UTILS
|
|
|
|
| 423 |
|
| 424 |
|
| 425 |
# -----------------------------------------------------------------------------
|
| 426 |
+
# ODYSSEUS ENGINE V2: SYSTEM DYNAMICS (SCIENTIFIC EQUILIBRIUM)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
# -----------------------------------------------------------------------------
|
| 428 |
|
| 429 |
+
def validate_odysseus_v2(t: dict) -> (bool, str):
|
| 430 |
+
"""Ensures the AI output is a valid mathematical simulation."""
|
| 431 |
+
try:
|
| 432 |
+
if t.get("engine", {}).get("version") != "v2": return False, "Version Mismatch"
|
| 433 |
+
render_types = {"biological_growth", "chemical_reactor", "mechanical_stress", "vector_flow"}
|
| 434 |
+
if t.get("simulation_type") not in render_types: return False, "Invalid visualizer"
|
| 435 |
+
|
| 436 |
+
logic = t.get("logic", {})
|
| 437 |
+
if "equilibrium_window" not in logic or "entropy_factor" not in logic:
|
| 438 |
+
return False, "Missing physics constants"
|
| 439 |
+
|
| 440 |
+
return True, "ok"
|
| 441 |
+
except: return False, "Schema corruption"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
@app.route('/api/trial/generate', methods=['POST'])
|
| 444 |
def generate_trial():
|
| 445 |
+
"""
|
| 446 |
+
Odysseus v2: Designs a System Dynamics simulation based on First Principles.
|
| 447 |
+
Analyzes the Epiphany layer to identify a 'Tension' (e.g. Gravity vs Lift).
|
| 448 |
+
"""
|
| 449 |
+
logger.info(">>> ODYSSEUS V2 TRIAL GENERATION")
|
| 450 |
uid = verify_token(request.headers.get('Authorization'))
|
| 451 |
+
if not uid: return jsonify({"error": "Unauthorized"}), 401
|
| 452 |
+
|
| 453 |
+
payload = request.get_json()
|
| 454 |
+
ep_id = payload.get("epiphanyId")
|
| 455 |
+
layer_key = payload.get("layerKey") # genesis | scientific_core | engineering_edge | cross_pollination
|
|
|
|
| 456 |
subject = payload.get("subject")
|
| 457 |
|
| 458 |
+
# 1. Persistent Cache Check
|
| 459 |
+
trial_path = f"epiphanies/{ep_id}/trials/{layer_key}"
|
| 460 |
+
existing = db_ref.child(trial_path).get()
|
| 461 |
+
if existing: return jsonify(existing), 200
|
| 462 |
+
|
| 463 |
+
# 2. Transactional Lock (Prevent double-spending/concurrency)
|
| 464 |
+
lock_ref = db_ref.child(f"epiphanies/{ep_id}/trialLocks/{layer_key}")
|
| 465 |
+
if lock_ref.get(): return jsonify({"error": "Simulation initializing..."}), 409
|
| 466 |
+
lock_ref.set({"uid": uid, "at": datetime.utcnow().isoformat()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
try:
|
| 469 |
+
# 3. High-Intellect Physics Prompting
|
| 470 |
+
# We fetch the actual text of the layer for specific context
|
| 471 |
+
layer_data = db_ref.child(f"epiphanies/{ep_id}/layers/{layer_key}").get() or {}
|
| 472 |
+
context_text = layer_data.get('text', '')
|
| 473 |
+
|
| 474 |
+
trial_prompt = f"""
|
| 475 |
+
Act as a Scientific Simulation Designer. Create an Odysseus v2 Trial for: {subject}.
|
| 476 |
+
Layer Focus: {layer_key}. Context: {context_text}
|
| 477 |
+
|
| 478 |
+
Task: Identify the core 'Scientific Tension' of this system (e.g. Photosynthesis balance, Structural load, Ionization equilibrium).
|
| 479 |
+
|
| 480 |
+
MANDATORY JSON SCHEMA:
|
| 481 |
+
{{
|
| 482 |
+
"engine": {{"name": "odysseus", "version": "v2"}},
|
| 483 |
+
"simulation_type": "biological_growth | chemical_reactor | mechanical_stress | vector_flow",
|
| 484 |
+
"labels": {{
|
| 485 |
+
"title": "Feynman-style Title",
|
| 486 |
+
"goal": "Explain the goal simply.",
|
| 487 |
+
"slider_label": "The force the user controls (e.g. 'Photon Intensity', 'Structural Rigidity')"
|
| 488 |
+
}},
|
| 489 |
+
"logic": {{
|
| 490 |
+
"initial_value": 50,
|
| 491 |
+
"target_value": 75,
|
| 492 |
+
"equilibrium_window": 10,
|
| 493 |
+
"entropy_factor": 0.5,
|
| 494 |
+
"reaction_latency": 0.2,
|
| 495 |
+
"hold_time_ms": 3000
|
| 496 |
+
}},
|
| 497 |
+
"narrative": {{
|
| 498 |
+
"on_win": "A Feynman-style explanation of why they succeeded.",
|
| 499 |
+
"on_fail": "A Socratic explanation of why the system collapsed."
|
| 500 |
+
}}
|
| 501 |
+
}}
|
| 502 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
+
res = client.models.generate_content(
|
| 505 |
+
model=ATHENA_FLASH,
|
| 506 |
+
contents=trial_prompt,
|
| 507 |
+
config=types.GenerateContentConfig(response_mime_type="application/json")
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
trial_data = json.loads(_strip_json_fences(res.text))
|
| 511 |
+
|
| 512 |
+
# 4. Validation & Spark Charge
|
| 513 |
+
ok, why = validate_odysseus_v2(trial_data)
|
| 514 |
+
if not ok:
|
| 515 |
+
lock_ref.delete()
|
| 516 |
+
return jsonify({"error": f"Logic failure: {why}"}), 422
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
+
user_ref = db_ref.child(f'users/{uid}')
|
| 519 |
+
credits = user_ref.get().get('credits', 0)
|
| 520 |
+
if credits < 1:
|
| 521 |
+
lock_ref.delete()
|
| 522 |
+
return jsonify({"error": "Insufficient Sparks"}), 402
|
| 523 |
+
|
| 524 |
+
# 5. Finalize & Persist
|
| 525 |
+
trial_data["createdAt"] = datetime.utcnow().isoformat()
|
| 526 |
+
db_ref.child(trial_path).set(trial_data)
|
| 527 |
+
user_ref.update({'credits': credits - 1})
|
| 528 |
+
|
| 529 |
+
lock_ref.delete()
|
| 530 |
+
logger.info(f"Odysseus v2 Trial manifested for {subject}")
|
| 531 |
+
return jsonify(trial_data), 201
|
| 532 |
+
|
| 533 |
+
except Exception as e:
|
| 534 |
+
lock_ref.delete()
|
| 535 |
+
logger.error(f"Trial Gen Error: {e}")
|
| 536 |
+
return jsonify({"error": "Failed to compile physics logic"}), 500
|
| 537 |
|
| 538 |
# -----------------------------------------------------------------------------
|
| 539 |
# 5. THE CHIRON MENTOR & SYSTEM UTILS
|