GeoAI-Backend / app.py
Rafs-an09002's picture
sync: backend from GitHub Actions
46ec770 verified
"""
GeoAI Backend β€” Flask REST API
"""
import os
import logging
from flask import Flask, request, jsonify
from flask_cors import CORS
from dotenv import load_dotenv
load_dotenv()
from config import DEPLOYMENT_CONFIG
from core.inference_engine import InferenceEngine
from utils.data_loader import DataLoader
from utils.logger import setup_logger
logger = setup_logger('geoai', level=DEPLOYMENT_CONFIG['log_level'])
app = Flask(__name__)
CORS(app)
data_loader = DataLoader(data_dir='data')
inference_engine = InferenceEngine()
# ── Utility ───────────────────────────────────────────────────────────────────
def _get_game_state(session_id: str):
if not session_id:
return None, jsonify({'error': 'session_id required'}), 400
game_state = inference_engine.get_game_state(session_id)
if not game_state:
inference_engine.active_games.pop(session_id, None)
return None, jsonify({'error': 'Invalid or expired session'}), 400
return game_state, None, None
def _top_countries(game_state, n: int = 10):
active = game_state.get_active_items()
ranked = sorted(active, key=lambda x: x.probability, reverse=True)
return [
{'name': item.name, 'prob': round(item.probability * 100, 2)}
for item in ranked[:n]
]
# ── Data endpoints (for frontend to load JSON) ────────────────────────────────
@app.route('/api/data/countries', methods=['GET'])
def get_countries():
data = data_loader.get_category_data('country')
return jsonify(data)
@app.route('/api/data/cities', methods=['GET'])
def get_cities():
data = data_loader.get_category_data('city')
return jsonify(data)
@app.route('/api/data/places', methods=['GET'])
def get_places():
data = data_loader.get_category_data('place')
return jsonify(data)
@app.route('/api/data/questions', methods=['GET'])
def get_questions():
data = data_loader.get_all_questions()
return jsonify(data)
# ── Health ────────────────────────────────────────────────────────────────────
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({
'status': 'healthy',
'version': DEPLOYMENT_CONFIG['version'],
'engine': 'GeoAI Bayesian AI v1.3.0',
'data_stats': data_loader.get_data_stats()
})
# ── Game endpoints ────────────────────────────────────────────────────────────
@app.route('/api/start-game', methods=['POST'])
def start_game():
try:
body = request.json
category = body.get('category')
questions = body.get('questions', [])
if not category:
return jsonify({'error': 'category required'}), 400
items = data_loader.get_category_data(category)
if not items:
return jsonify({'error': f'No data for category: {category}'}), 400
if not questions:
return jsonify({'error': 'Question bank required from frontend'}), 400
game_state = inference_engine.start_new_game(category, items, questions)
logger.info(f"Game started: {game_state.session_id} | {len(items)} items")
return jsonify({
'session_id': game_state.session_id,
'category': category,
'total_items': len(items),
'questions_available': len(questions),
'message': 'Game started successfully'
})
except Exception as e:
logger.error(f"start_game error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/question', methods=['POST'])
def get_next_question():
try:
session_id = request.json.get('session_id')
game_state, err, status = _get_game_state(session_id)
if err:
return err, status
question = inference_engine.get_next_question(game_state)
active_items = game_state.get_active_items()
confidence = inference_engine.confidence_calculator.calculate(active_items)
top_item = game_state.get_top_prediction()
ready = question is None
return jsonify({
'question': question,
'ready_to_guess': ready,
'confidence': round(confidence, 1),
'questions_asked': game_state.questions_asked,
'active_items_count': len(active_items),
'top_guess': top_item.name if top_item else None,
'top_countries': _top_countries(game_state),
})
except Exception as e:
logger.error(f"get_next_question error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/answer', methods=['POST'])
def process_answer():
try:
body = request.json
session_id = body.get('session_id')
answer = body.get('answer')
game_state, err, status = _get_game_state(session_id)
if err:
return err, status
if not answer:
return jsonify({'error': 'answer required'}), 400
result = inference_engine.process_answer(game_state, answer)
result['top_countries'] = _top_countries(game_state)
return jsonify(result)
except Exception as e:
logger.error(f"process_answer error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/predict', methods=['POST'])
def get_prediction():
try:
session_id = request.json.get('session_id')
game_state, err, status = _get_game_state(session_id)
if err:
return err, status
result = inference_engine.get_final_prediction(game_state)
inference_engine.firebase_service.delete_game_state(session_id)
return jsonify(result)
except Exception as e:
logger.error(f"get_prediction error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/feedback', methods=['POST'])
def submit_feedback():
try:
body = request.json
session_id = body.get('session_id')
actual_answer_name = body.get('actual_answer')
game_state, err, status = _get_game_state(session_id)
if err:
return err, status
top_item = game_state.get_top_prediction()
actual_item = next(
(i for i in game_state.items
if i.name.lower() == (actual_answer_name or '').lower()),
None
)
if actual_item:
inference_engine.firebase_service.log_game_result(
game_state,
top_item.name if top_item else 'None',
inference_engine.confidence_calculator.calculate(game_state.get_active_items()),
was_correct=False,
failure_reason='user_correction',
actual_answer=actual_answer_name
)
for item in game_state.items:
if item.id == actual_item.id:
item.probability *= 20.0
item.eliminated = False
else:
item.probability *= 0.05
inference_engine.probability_manager.normalize_probabilities(game_state.items)
inference_engine.probability_manager.soft_filter(game_state.items)
inference_engine.firebase_service.save_game_state(game_state)
return jsonify({
'status': 'learning_in_progress',
'message': 'AI learned from the mistake and will continue.',
'questions_asked': game_state.questions_asked,
'top_countries': _top_countries(game_state),
})
except Exception as e:
logger.error(f"submit_feedback error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/stats', methods=['GET'])
def get_stats():
try:
return jsonify({
'local_session_stats': inference_engine.get_session_stats(),
'data_stats': data_loader.get_data_stats(),
'config': {
'version': DEPLOYMENT_CONFIG['version'],
'max_questions': DEPLOYMENT_CONFIG.get('max_questions', 'unlimited'),
}
})
except Exception as e:
logger.error(f"get_stats error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# ── Error handlers ────────────────────────────────────────────────────────────
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Endpoint not found'}), 404
@app.errorhandler(500)
def internal_error(e):
logger.error("Internal Server Error: %s", e, exc_info=True)
return jsonify({'error': 'Internal server error'}), 500
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == '__main__':
port = DEPLOYMENT_CONFIG['port']
debug = DEPLOYMENT_CONFIG['debug']
logger.info(f"Starting GeoAI Backend on port {port} (debug={debug})")
app.run(host='0.0.0.0', port=port, debug=debug)