aki / main.py
tachibanaa710's picture
Update main.py
e1ccd25 verified
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):
@wraps(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)}")
@app.get("/start_game", response_model=GameResponse)
@retry_on_session_error(max_retries=3, delay=0.2)
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)}")
@app.get("/answer", response_model=GameResponse)
@retry_on_session_error(max_retries=5, delay=0.2)
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)}")
@app.get("/back", response_model=GameResponse)
@retry_on_session_error(max_retries=5, delay=0.2)
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))
@app.get("/choose", response_model=GameResponse)
@retry_on_session_error(max_retries=5, delay=0.2)
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))
@app.get("/exclude", response_model=GameResponse)
@retry_on_session_error(max_retries=5, delay=0.2)
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))
@app.get("/session_info")
@retry_on_session_error(max_retries=3, delay=0.1)
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))
@app.get("/health")
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
@app.get("/debug")
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)