from flask import Flask, request, jsonify from flask_cors import CORS import os from dotenv import load_dotenv # Import our Agents from agents.puzzle_agent import PuzzleAgent from agents.adventure_agent import AdventureAgent from agents.adaptive_agent import AdaptiveAgent from agents.hint_agent import HintAgent # Load environment variables. Do not override space secrets (like GEMINI_API_KEY) already set in the environment. if os.environ.get("GEMINI_API_KEY"): load_dotenv(override=False) else: load_dotenv(override=True) app = Flask(__name__) CORS(app) # Enable CORS for all routes # Initialize Agents puzzle_agent = PuzzleAgent() adventure_agent = AdventureAgent() adaptive_agent = AdaptiveAgent() hint_agent = HintAgent() @app.route('/', methods=['GET']) def home(): return jsonify({ "status": "online", "message": "CodeCracker AI Backend is Running!", "endpoints": { "health_check": "/health", "generate_level": "/api/level/generate", "feedback": "/api/level/feedback" } }) @app.route('/health', methods=['GET']) def health_check(): return jsonify({"status": "healthy", "service": "CodeCracker AI Backend"}) @app.route('/ping', methods=['GET']) def ping(): """ Lightweight keep-alive endpoint. Call this every 25 minutes from an external cron (e.g. cron-job.org, UptimeRobot) to prevent the HF Space from going to sleep on the free tier. URL to ping: https://faaizashiq-codecracker-backend.hf.space/ping """ return jsonify({"pong": True}) @app.route('/api/level/generate', methods=['POST']) def generate_level(): """ Generates a new level (Maze, Blockly, or Time Challenge). Expected JSON: { "mode": "maze", "difficulty": "Easy", "topic": "Space" (optional) } """ data = request.json or {} mode = data.get('mode', 'maze') difficulty = data.get('difficulty', 'Easy') topic = data.get('topic') try: if mode == 'maze': level_data = puzzle_agent.generate_level(difficulty, topic) else: level_data = adventure_agent.generate_level(mode, difficulty, topic) if not level_data: raise Exception("Empty level data returned") # Validate the response has the right shape before sending # The agent's own fallback already returns {"story_arc_title":..., "levels":[...]} # so this should always pass. Log a warning if it doesn't. if mode != 'maze' and 'levels' not in level_data: print(f"WARNING: agent returned data without 'levels' key: {list(level_data.keys())}") return jsonify(level_data) except Exception as e: print(f"CRITICAL ERROR /api/level/generate: {e}") # Return a mode-appropriate fallback — NOT a maze object for blockly requests if mode == 'blockly': fallback = { "story_arc_title": "System Offline Arc", "levels": [ { "type": "blockly", "level_id": f"emergency_{i}", "title": f"System Repair Part {i}", "story": "The AI systems are offline. Practice mode engaged.", "concept_tutorial": "Loops: Repeat actions without writing code over and over.", "problem": "Create a loop that prints 1, 2, 3.", "toolbox_categories": ["Loops", "Math", "Variables", "Text"], "initial_code": "", "validation_rules": { "required_concepts": ["loop"], "expected_output": "1\n2\n3" }, "hint_1": "Use the 'repeat' block.", "hint_2": "Set the number to 3." } for i in range(1, 6) ] } elif mode == 'time_challenge': fallback = { "story_arc_title": "System Reboot Arc", "levels": [ { "type": "time_challenge", "level_id": f"emergency_tc_{i}", "title": f"System Reboot Part {i}", "story": "Critical Error! Debug the startup sequence.", "concept_tutorial": "Syntax: Code must follow strict grammar rules.", "timer_seconds": 60, "buggy_code": "print('System Ready'\nstart_engine()", "task": "Fix the syntax error.", "solution_patch": "print('System Ready')\nstart_engine()" } for i in range(1, 6) ] } else: # maze fallback — single level is fine here fallback = { "story_arc_title": "Emergency Training", "levels": [{ "type": "maze", "level_id": "emergency_fallback", "title": "System Practice", "story": "System offline. Practice mode engaged.", "grid_layout": [[2, 0, 3]], "maze_rows": 1, "maze_cols": 3, "ordered_steps": [], "cell_values": {}, "maze_rule": {"type": "none"}, "allowed_blocks": ["move_right"], "toolbox_xml": '', "theme_palette": {"wall_color": "#8E24AA", "path_color": "#020817", "accent": "#FF477E"} }] } return jsonify(fallback) @app.route('/api/level/feedback', methods=['POST']) def level_feedback(): """ Receives user feedback (1-5 stars) on a level. Expected JSON: { "level_data": {...}, "rating": 5 } """ data = request.json or {} level_data = data.get('level_data') rating = data.get('rating') developer_feedback = data.get('developer_feedback') if not level_data or not rating: return jsonify({"message": "Invalid data ignored"}), 400 try: # Teach the agent! if level_data.get('type') == 'maze': puzzle_agent.learn_from_feedback(level_data, int(rating), developer_feedback) else: adventure_agent.learn_from_feedback(level_data, int(rating), developer_feedback) return jsonify({"status": "learned", "message": "Thanks for the feedback!"}) except Exception as e: print(f"Feedback Error: {e}") return jsonify({"status": "ignored"}), 200 @app.route('/api/player/evaluate', methods=['POST']) def evaluate_player(): """ Evaluates player history to adjust difficulty. Expected JSON: { "history": [ { "result": "success", "time_taken": 30 }, ... ] } """ data = request.json or {} history = data.get('history', []) try: evaluation = adaptive_agent.evaluate_user(history) return jsonify(evaluation) except Exception as e: print(f"CRITICAL ERROR /api/player/evaluate: {e}") # Fallback: Default to current or Easy return jsonify({"difficulty": "Easy", "reason": "System requires calibration."}) @app.route('/api/hint/generate', methods=['POST']) def generate_hint(): """ Generates a context-aware hint. Expected JSON: { "mode": "maze|blockly", "level_context": {...}, "user_state": {...} } """ data = request.json or {} mode = data.get('mode', 'blockly') user_state = data.get('user_state', {}) try: hint = hint_agent.generate_hint( mode=mode, level_context=data.get('level_context', {}), user_state=user_state ) return jsonify({"hint": hint}) except Exception as e: print(f"CRITICAL ERROR /api/hint/generate: {e}") return jsonify({"hint": "Try checking your logic step-by-step."}) if __name__ == '__main__': port = int(os.environ.get('PORT', 5000)) app.run(host='0.0.0.0', port=port, debug=True)