rairo commited on
Commit
1cb623e
·
verified ·
1 Parent(s): eb773e5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +99 -321
main.py CHANGED
@@ -423,339 +423,117 @@ def deep_dive():
423
 
424
 
425
  # -----------------------------------------------------------------------------
426
- # ODYSSEUS TRIAL ENGINE: REASONING-BASED GAME GENERATOR (NO FALLBACK)
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
- TRIAL_RENDERERS = {"thermal", "electrical", "mechanical", "chemical", "biological"}
435
- TRIAL_GRAMMARS = {"accumulate", "balance", "transform"}
436
-
437
- def _strip_json_fences(s: str) -> str:
438
- s = (s or "").strip()
439
- if "```" in s:
440
- m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.DOTALL)
441
- if m:
442
- return m.group(1).strip()
443
- return s
444
-
445
- def _is_num(x):
446
- return isinstance(x, (int, float)) and not isinstance(x, bool)
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
- logger.info(">>> ODYSSEUS TRIAL GENERATION INITIATED")
554
-
555
- # 1) AUTH
 
 
556
  uid = verify_token(request.headers.get('Authorization'))
557
- if not uid:
558
- return jsonify({"error": "Unauthorized"}), 401
559
-
560
- payload = request.get_json(silent=True) or {}
561
- epiphany_id = payload.get("epiphanyId")
562
- layer_key = payload.get("layerKey") # genesis | scientific_core | engineering_edge | cross_pollination
563
  subject = payload.get("subject")
564
 
565
- if not all([epiphany_id, layer_key, subject]):
566
- return jsonify({"error": "Missing context (epiphanyId, layerKey, subject)."}), 400
567
-
568
- # 2) OWNERSHIP CHECK
569
- ep_ref = db_ref.child(f"epiphanies/{epiphany_id}")
570
- ep = ep_ref.get() or {}
571
- if not ep or ep.get("uid") != uid:
572
- return jsonify({"error": "Forbidden: epiphany not found or not owned by user."}), 403
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
- trial_json = None
658
-
659
- for _ in range(2):
660
- attempts += 1
661
- try:
662
- res = client.models.generate_content(
663
- model=model_name,
664
- contents=trial_prompt,
665
- config=types.GenerateContentConfig(response_mime_type="application/json")
666
- )
667
- raw = _strip_json_fences(res.text)
668
- raw_outputs.append(raw[:2000]) # keep a preview for logging
669
-
670
- candidate = json.loads(raw)
671
- candidate["createdAt"] = datetime.utcnow().isoformat()
672
-
673
- ok, why = validate_trial_schema(candidate)
674
- if ok:
675
- trial_json = candidate
676
- break
677
- last_validation_error = why
678
-
679
- except Exception as e:
680
- last_exception = str(e)
681
-
682
- # 6) IF INVALID: LOG FAILURE + RETURN 422 (no charge)
683
- if not trial_json:
684
- failure_doc = {
685
- "status": "failed",
686
- "uid": uid,
687
- "epiphanyId": epiphany_id,
688
- "layerKey": layer_key,
689
- "subject": subject,
690
- "model": model_name,
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
- except Exception as e:
740
- # Hard failure path (still no fallback)
741
- failure_doc = {
742
- "status": "failed",
743
- "uid": uid,
744
- "epiphanyId": epiphany_id,
745
- "layerKey": layer_key,
746
- "subject": subject,
747
- "model": model_name,
748
- "attempts": attempts,
749
- "validation_error": last_validation_error,
750
- "exception": str(e),
751
- "raw_output_preview": raw_outputs[-1] if raw_outputs else None,
752
- "at": datetime.utcnow().isoformat()
753
- }
754
- db_ref.child(f"trial_failures/{uid}/{epiphany_id}/{layer_key}").push(failure_doc)
755
 
756
- cleanup_lock_best_effort()
757
- logger.error(f"Trial Generation Failure: {e}")
758
- return jsonify({"error": "Trial generation failed."}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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