Spaces:
Paused
Paused
| from fastapi import FastAPI, HTTPException, Query | |
| from fastapi.responses import JSONResponse | |
| from typing import Optional, Literal, Dict, Any | |
| import akinator | |
| import logging | |
| import uvicorn | |
| from pydantic import BaseModel | |
| import json | |
| import time | |
| import asyncio | |
| from functools import wraps | |
| from html import unescape | |
| from cloudscraper import CloudScraper, create_scraper | |
| import pickle | |
| import base64 | |
| app = FastAPI(title="Akinator API") | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class GameResponse(BaseModel): | |
| cloudscraper_session: str # Base64 encoded session data | |
| question: str | |
| progression: float | |
| step: int | |
| finished: bool | |
| win: Optional[bool] = None | |
| confidence: float | |
| theme_name: str | |
| akitude_url: str | |
| # Results when finished | |
| proposition: Optional[str] = None | |
| name_proposition: Optional[str] = None | |
| description_proposition: Optional[str] = None | |
| photo: Optional[str] = None | |
| pseudo: Optional[str] = None | |
| def serialize_akinator_session(aki: akinator.Akinator) -> str: | |
| """Serialize Akinator session (including CloudScraper session) to base64 string""" | |
| try: | |
| # Handle cookies with potential duplicates | |
| cookies_dict = {} | |
| if hasattr(aki.session, 'cookies') and aki.session.cookies: | |
| for cookie in aki.session.cookies: | |
| # Use the last value if there are duplicates | |
| cookies_dict[cookie.name] = cookie.value | |
| # Get the CloudScraper session data | |
| cloudscraper_data = { | |
| 'cookies': cookies_dict, | |
| 'headers': dict(aki.session.headers) if hasattr(aki.session, 'headers') else {}, | |
| 'proxies': getattr(aki.session, 'proxies', None), | |
| 'auth': getattr(aki.session, 'auth', None), | |
| 'verify': getattr(aki.session, 'verify', True), | |
| 'stream': getattr(aki.session, 'stream', False), | |
| 'cert': getattr(aki.session, 'cert', None), | |
| 'max_redirects': getattr(aki.session, 'max_redirects', 30), | |
| 'trust_env': getattr(aki.session, 'trust_env', True), | |
| } | |
| # Get the Akinator session data | |
| akinator_data = { | |
| 'session_id': getattr(aki, 'session_id', ''), | |
| 'signature': getattr(aki, 'signature', ''), | |
| 'identifiant': getattr(aki, 'identifiant', ''), | |
| 'step': getattr(aki, 'step', 0), | |
| 'progression': getattr(aki, 'progression', 0.0), | |
| 'question': getattr(aki, 'question', ''), | |
| 'finished': getattr(aki, 'finished', False), | |
| 'win': getattr(aki, 'win', None), | |
| 'theme': getattr(aki, 'theme', 'c'), | |
| 'language': getattr(aki, 'language', 'en'), | |
| 'child_mode': getattr(aki, 'child_mode', False), | |
| 'step_last_proposition': getattr(aki, 'step_last_proposition', None), | |
| 'id_proposition': getattr(aki, 'id_proposition', None), | |
| 'name_proposition': getattr(aki, 'name_proposition', None), | |
| 'description_proposition': getattr(aki, 'description_proposition', None), | |
| 'proposition': getattr(aki, 'proposition', None), | |
| 'photo': getattr(aki, 'photo', None), | |
| 'pseudo': getattr(aki, 'pseudo', None), | |
| 'completion': getattr(aki, 'completion', None), | |
| 'akitude': getattr(aki, 'akitude', None), | |
| 'flag_photo': getattr(aki, 'flag_photo', None), | |
| } | |
| # Combine both session data | |
| combined_data = { | |
| 'cloudscraper_session': cloudscraper_data, | |
| 'akinator_session': akinator_data, | |
| 'timestamp': time.time() | |
| } | |
| # Serialize to bytes then base64 with error handling | |
| try: | |
| serialized = pickle.dumps(combined_data) | |
| return base64.b64encode(serialized).decode('utf-8') | |
| except Exception as pickle_error: | |
| logger.error(f"Pickle serialization failed: {pickle_error}") | |
| # Try with minimal data if full serialization fails | |
| minimal_data = { | |
| 'cloudscraper_session': cloudscraper_data, | |
| 'akinator_session': { | |
| 'step': akinator_data.get('step', 0), | |
| 'progression': akinator_data.get('progression', 0.0), | |
| 'question': akinator_data.get('question', ''), | |
| 'finished': akinator_data.get('finished', False), | |
| 'theme': akinator_data.get('theme', 'c'), | |
| 'language': akinator_data.get('language', 'en'), | |
| }, | |
| 'timestamp': time.time() | |
| } | |
| serialized = pickle.dumps(minimal_data) | |
| return base64.b64encode(serialized).decode('utf-8') | |
| except Exception as e: | |
| logger.error(f"Failed to serialize session: {e}") | |
| # Return empty string if serialization completely fails | |
| return "" | |
| def deserialize_akinator_session(session_data: str) -> akinator.Akinator: | |
| """Deserialize base64 string to Akinator instance with restored session""" | |
| try: | |
| if not session_data: | |
| # Create new session if no data provided | |
| cloudscraper_session = create_scraper() | |
| return akinator.Akinator(session=cloudscraper_session) | |
| # Decode base64 then deserialize | |
| decoded = base64.b64decode(session_data.encode('utf-8')) | |
| data = pickle.loads(decoded) | |
| # Restore CloudScraper session | |
| scraper = create_scraper() | |
| cloudscraper_data = data.get('cloudscraper_session', {}) | |
| # Restore cookies - handle them as a simple dict | |
| if 'cookies' in cloudscraper_data and cloudscraper_data['cookies']: | |
| for name, value in cloudscraper_data['cookies'].items(): | |
| scraper.cookies[name] = value | |
| # Restore headers with error handling | |
| if 'headers' in cloudscraper_data and cloudscraper_data['headers']: | |
| try: | |
| scraper.headers.update(cloudscraper_data['headers']) | |
| except Exception as e: | |
| logger.warning(f"Failed to restore headers: {e}") | |
| # Restore other attributes with error handling | |
| safe_attrs = ['proxies', 'auth', 'verify', 'stream', 'cert', 'max_redirects', 'trust_env'] | |
| for attr in safe_attrs: | |
| if attr in cloudscraper_data: | |
| try: | |
| value = cloudscraper_data[attr] | |
| if value is not None or attr in ['verify', 'stream', 'trust_env']: | |
| setattr(scraper, attr, value) | |
| except Exception as e: | |
| logger.warning(f"Failed to restore {attr}: {e}") | |
| # Create akinator instance with restored CloudScraper session | |
| aki = akinator.Akinator(session=scraper) | |
| # Restore akinator attributes | |
| akinator_data = data.get('akinator_session', {}) | |
| if not akinator_data: | |
| logger.warning("No akinator session data found, returning new instance") | |
| return aki | |
| read_only_properties = {'confidence', 'theme_id', 'theme_name', 'akitude_url'} | |
| for key, value in akinator_data.items(): | |
| if key not in read_only_properties: | |
| try: | |
| setattr(aki, key, value) | |
| except (AttributeError, TypeError) as e: | |
| logger.warning(f"Could not restore attribute {key}: {e}") | |
| continue | |
| # Validate that we have essential data | |
| if not hasattr(aki, 'step') or not hasattr(aki, 'progression'): | |
| logger.warning("Essential session data missing, creating new session") | |
| return akinator.Akinator(session=scraper) | |
| return aki | |
| except Exception as e: | |
| logger.error(f"Failed to deserialize session: {e}") | |
| # Return new instance if deserialization fails | |
| try: | |
| cloudscraper_session = create_scraper() | |
| return akinator.Akinator(session=cloudscraper_session) | |
| except Exception as fallback_error: | |
| logger.error(f"Even fallback failed: {fallback_error}") | |
| raise HTTPException(status_code=500, detail=f"Failed to create session: {str(fallback_error)}") | |
| def retry_on_session_error(max_retries: int = 3, delay: float = 0.1): | |
| """Decorator to retry operations that might fail due to session issues""" | |
| def decorator(func): | |
| async def wrapper(*args, **kwargs): | |
| last_exception = None | |
| for attempt in range(max_retries): | |
| try: | |
| return await func(*args, **kwargs) | |
| except HTTPException as e: | |
| # Handle specific session-related errors | |
| if e.status_code == 500 and "session" in str(e.detail).lower(): | |
| last_exception = e | |
| if attempt < max_retries - 1: | |
| logger.warning(f"Session error, retrying... (attempt {attempt + 1}/{max_retries}): {e.detail}") | |
| await asyncio.sleep(delay * (2 ** attempt)) # Exponential backoff | |
| continue | |
| raise # Re-raise if it's not a session-related error | |
| except (akinator.exceptions.AkinatorException, | |
| akinator.exceptions.InvalidAnswerError, | |
| akinator.exceptions.CantGoBackAnyFurther) as e: | |
| # Handle specific akinator exceptions | |
| if "session" in str(e).lower() or "timeout" in str(e).lower(): | |
| last_exception = HTTPException(status_code=500, detail=str(e)) | |
| if attempt < max_retries - 1: | |
| logger.warning(f"Akinator session error, retrying... (attempt {attempt + 1}/{max_retries}): {e}") | |
| await asyncio.sleep(delay * (2 ** attempt)) | |
| continue | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except Exception as e: | |
| last_exception = e | |
| if attempt < max_retries - 1: | |
| logger.warning(f"Unexpected error, retrying... (attempt {attempt + 1}/{max_retries}): {e}") | |
| await asyncio.sleep(delay * (2 ** attempt)) | |
| continue | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # If we get here, all retries failed | |
| if last_exception: | |
| if isinstance(last_exception, HTTPException): | |
| raise last_exception | |
| else: | |
| raise HTTPException(status_code=500, detail=str(last_exception)) | |
| return wrapper | |
| return decorator | |
| def create_response(aki: akinator.Akinator) -> GameResponse: | |
| """Create response from akinator instance with error handling""" | |
| try: | |
| # Serialize the session with validation | |
| cloudscraper_session = serialize_akinator_session(aki) | |
| # If serialization failed, log but still return response | |
| if not cloudscraper_session: | |
| logger.warning("Session serialization failed, response will not contain session data") | |
| return GameResponse( | |
| cloudscraper_session=cloudscraper_session, | |
| question=getattr(aki, 'question', ''), | |
| progression=getattr(aki, 'progression', 0.0), | |
| step=getattr(aki, 'step', 0), | |
| finished=getattr(aki, 'finished', False), | |
| win=getattr(aki, 'win', None), | |
| confidence=getattr(aki, 'confidence', 0.0), | |
| theme_name=getattr(aki, 'theme_name', ''), | |
| akitude_url=getattr(aki, 'akitude_url', ''), | |
| proposition=getattr(aki, 'proposition', None) if getattr(aki, 'finished', False) else None, | |
| name_proposition=getattr(aki, 'name_proposition', None) if getattr(aki, 'finished', False) else None, | |
| description_proposition=getattr(aki, 'description_proposition', None) if getattr(aki, 'finished', False) else None, | |
| photo=getattr(aki, 'photo', None) if getattr(aki, 'finished', False) else None, | |
| pseudo=getattr(aki, 'pseudo', None) if getattr(aki, 'finished', False) else None | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error creating response: {e}") | |
| raise HTTPException(status_code=500, detail=f"Error creating response: {str(e)}") | |
| async def start_game( | |
| language: str = Query("en", description="Language code"), | |
| child_mode: bool = Query(False, description="Child mode"), | |
| theme: Literal["c", "a", "o"] = Query("c", description="Theme") | |
| ): | |
| """Start new game session""" | |
| try: | |
| # Create CloudScraper session | |
| cloudscraper_session = create_scraper() | |
| # Create akinator instance with CloudScraper session | |
| aki = akinator.Akinator(session=cloudscraper_session) | |
| aki.start_game(language=language, child_mode=child_mode, theme=theme) | |
| logger.info(f"New game started with theme: {theme}, language: {language}") | |
| return create_response(aki) | |
| except Exception as e: | |
| logger.error(f"Error starting game: {e}") | |
| raise HTTPException(status_code=500, detail=f"Failed to start game: {str(e)}") | |
| async def answer_question( | |
| answer: str = Query(..., description="Answer: yes/no/i don't know/probably/probably not"), | |
| scraper_session: str = Query(..., description="Base64 encoded session data") | |
| ): | |
| """Answer current question""" | |
| try: | |
| # Validate answer - Fix case sensitivity issues | |
| valid_answers = ['yes', 'no', 'i don\'t know', 'probably', 'probably not'] | |
| answer_lower = answer.lower() | |
| if answer_lower not in [a.lower() for a in valid_answers]: | |
| raise HTTPException(status_code=400, detail=f"Invalid answer. Use: {', '.join(valid_answers)}") | |
| # Restore akinator session | |
| aki = deserialize_akinator_session(scraper_session) | |
| # Verify session is in valid state | |
| if not hasattr(aki, 'question') or not aki.question: | |
| raise HTTPException(status_code=400, detail="Session in invalid state - no question available") | |
| # Check if the session has required attributes for making API calls | |
| if not hasattr(aki, 'session_id') or not aki.session_id: | |
| logger.warning("Session ID missing, attempting to restart game") | |
| # Try to restart the game to get fresh session | |
| aki.start_game(language=getattr(aki, 'language', 'en'), | |
| child_mode=getattr(aki, 'child_mode', False), | |
| theme=getattr(aki, 'theme', 'c')) | |
| # Add debug logging | |
| logger.info(f"Answering question at step {aki.step} with answer: {answer}") | |
| logger.info(f"Session ID: {getattr(aki, 'session_id', 'None')}") | |
| logger.info(f"Signature: {getattr(aki, 'signature', 'None')}") | |
| # Answer the question | |
| aki.answer(answer) | |
| logger.info(f"Question answered: {answer}, new step: {aki.step}") | |
| return create_response(aki) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error answering question: {e}") | |
| raise HTTPException(status_code=500, detail=f"Failed to process answer: {str(e)}") | |
| async def go_back( | |
| scraper_session: str = Query(..., description="Base64 encoded session data") | |
| ): | |
| """Go back to previous question""" | |
| try: | |
| # Restore akinator session | |
| aki = deserialize_akinator_session(scraper_session) | |
| # Check if we can go back | |
| if getattr(aki, 'step', 0) <= 0: | |
| raise HTTPException(status_code=400, detail="Can't go back any further") | |
| # Go back | |
| aki.back() | |
| logger.info(f"Went back to step {aki.step}") | |
| return create_response(aki) | |
| except akinator.CantGoBackAnyFurther: | |
| raise HTTPException(status_code=400, detail="Can't go back any further") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error going back: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def choose_proposition( | |
| scraper_session: str = Query(..., description="Base64 encoded session data") | |
| ): | |
| """Choose current proposition""" | |
| try: | |
| # Restore akinator session | |
| aki = deserialize_akinator_session(scraper_session) | |
| # Verify we have a proposition to choose | |
| if not getattr(aki, 'proposition', None): | |
| raise HTTPException(status_code=400, detail="No proposition available to choose") | |
| # Choose proposition | |
| aki.choose() | |
| logger.info(f"Proposition chosen: {aki.name_proposition}") | |
| return create_response(aki) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error choosing proposition: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def exclude_proposition( | |
| scraper_session: str = Query(..., description="Base64 encoded session data") | |
| ): | |
| """Exclude current proposition""" | |
| try: | |
| # Restore akinator session | |
| aki = deserialize_akinator_session(scraper_session) | |
| # Verify we have a proposition to exclude | |
| if not getattr(aki, 'proposition', None): | |
| raise HTTPException(status_code=400, detail="No proposition available to exclude") | |
| # Exclude proposition | |
| aki.exclude() | |
| logger.info(f"Proposition excluded: {aki.name_proposition}") | |
| return create_response(aki) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error excluding proposition: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_session_info( | |
| scraper_session: str = Query(..., description="Base64 encoded session data") | |
| ): | |
| """Get session information""" | |
| try: | |
| # Restore akinator session | |
| aki = deserialize_akinator_session(scraper_session) | |
| return { | |
| "step": getattr(aki, 'step', 0), | |
| "progression": getattr(aki, 'progression', 0.0), | |
| "finished": getattr(aki, 'finished', False), | |
| "theme_name": getattr(aki, 'theme_name', ''), | |
| "language": getattr(aki, 'language', 'en'), | |
| "confidence": getattr(aki, 'confidence', 0.0), | |
| "current_question": getattr(aki, 'question', ''), | |
| "has_proposition": bool(getattr(aki, 'proposition', None)), | |
| "session_id": getattr(aki, 'session_id', ''), | |
| "signature": getattr(aki, 'signature', ''), | |
| "identifiant": getattr(aki, 'identifiant', '') | |
| } | |
| except Exception as e: | |
| logger.error(f"Error getting session info: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return { | |
| "status": "healthy", | |
| "message": "Akinator API is running (session-less mode)", | |
| "timestamp": time.time() | |
| } | |
| # Debug endpoint to help troubleshoot | |
| async def debug_endpoint(): | |
| """Debug endpoint to test basic functionality""" | |
| try: | |
| # Test basic akinator functionality | |
| scraper = create_scraper() | |
| aki = akinator.Akinator(session=scraper) | |
| aki.start_game(language="en", child_mode=False, theme="c") | |
| return { | |
| "status": "success", | |
| "test_question": getattr(aki, 'question', 'No question'), | |
| "test_step": getattr(aki, 'step', 0), | |
| "test_progression": getattr(aki, 'progression', 0.0), | |
| "session_id": getattr(aki, 'session_id', 'None'), | |
| "signature": getattr(aki, 'signature', 'None') | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "error", | |
| "error": str(e) | |
| } | |
| if __name__ == "__main__": | |
| uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True) |