|
|
""" |
|
|
API routes for crossword puzzle generator. |
|
|
Matches the existing JavaScript API for frontend compatibility. |
|
|
""" |
|
|
|
|
|
import logging |
|
|
from typing import List, Dict, Any, Optional |
|
|
from datetime import datetime |
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, Depends |
|
|
from pydantic import BaseModel, Field |
|
|
|
|
|
from ..services.crossword_generator_wrapper import CrosswordGenerator |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter() |
|
|
|
|
|
|
|
|
class GeneratePuzzleRequest(BaseModel): |
|
|
topics: List[str] = Field(..., description="List of topics for the puzzle") |
|
|
difficulty: str = Field(default="medium", description="Difficulty level: easy, medium, hard") |
|
|
useAI: bool = Field(default=False, description="Use AI vector search for word generation") |
|
|
|
|
|
class WordInfo(BaseModel): |
|
|
word: str |
|
|
clue: str |
|
|
similarity: Optional[float] = None |
|
|
source: Optional[str] = None |
|
|
|
|
|
class ClueInfo(BaseModel): |
|
|
number: int |
|
|
word: str |
|
|
text: str |
|
|
direction: str |
|
|
position: Dict[str, int] |
|
|
|
|
|
class PuzzleMetadata(BaseModel): |
|
|
topics: List[str] |
|
|
difficulty: str |
|
|
wordCount: int |
|
|
size: int |
|
|
aiGenerated: bool |
|
|
|
|
|
class PuzzleResponse(BaseModel): |
|
|
grid: List[List[str]] |
|
|
clues: List[ClueInfo] |
|
|
metadata: PuzzleMetadata |
|
|
|
|
|
class TopicInfo(BaseModel): |
|
|
id: str |
|
|
name: str |
|
|
|
|
|
|
|
|
generator = None |
|
|
|
|
|
def get_crossword_generator(request: Request) -> CrosswordGenerator: |
|
|
"""Dependency to get the crossword generator with vector search service.""" |
|
|
global generator |
|
|
if generator is None: |
|
|
vector_service = getattr(request.app.state, 'vector_service', None) |
|
|
generator = CrosswordGenerator(vector_service) |
|
|
return generator |
|
|
|
|
|
@router.get("/topics", response_model=List[TopicInfo]) |
|
|
async def get_topics(): |
|
|
"""Get available topics for puzzle generation.""" |
|
|
|
|
|
topics = [ |
|
|
{"id": "animals", "name": "Animals"}, |
|
|
{"id": "geography", "name": "Geography"}, |
|
|
{"id": "science", "name": "Science"}, |
|
|
{"id": "technology", "name": "Technology"} |
|
|
] |
|
|
return topics |
|
|
|
|
|
@router.post("/generate", response_model=PuzzleResponse) |
|
|
async def generate_puzzle( |
|
|
request: GeneratePuzzleRequest, |
|
|
crossword_gen: CrosswordGenerator = Depends(get_crossword_generator) |
|
|
): |
|
|
""" |
|
|
Generate a crossword puzzle with optional AI vector search. |
|
|
|
|
|
This endpoint matches the JavaScript API exactly for frontend compatibility. |
|
|
""" |
|
|
try: |
|
|
logger.info(f"🎯 Generating puzzle for topics: {request.topics}, difficulty: {request.difficulty}, useAI: {request.useAI}") |
|
|
|
|
|
|
|
|
if not request.topics: |
|
|
raise HTTPException(status_code=400, detail="At least one topic is required") |
|
|
|
|
|
valid_difficulties = ["easy", "medium", "hard"] |
|
|
if request.difficulty not in valid_difficulties: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail=f"Invalid difficulty. Must be one of: {valid_difficulties}" |
|
|
) |
|
|
|
|
|
|
|
|
puzzle_data = await crossword_gen.generate_puzzle( |
|
|
topics=request.topics, |
|
|
difficulty=request.difficulty, |
|
|
use_ai=request.useAI |
|
|
) |
|
|
|
|
|
if not puzzle_data: |
|
|
raise HTTPException(status_code=500, detail="Failed to generate puzzle") |
|
|
|
|
|
logger.info(f"✅ Generated puzzle with {puzzle_data['metadata']['wordCount']} words") |
|
|
return puzzle_data |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
except Exception as e: |
|
|
logger.error(f"❌ Error generating puzzle: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@router.post("/words") |
|
|
async def generate_words( |
|
|
request: GeneratePuzzleRequest, |
|
|
crossword_gen: CrosswordGenerator = Depends(get_crossword_generator) |
|
|
): |
|
|
""" |
|
|
Generate words for given topics (debug endpoint). |
|
|
|
|
|
This endpoint allows testing word generation without full puzzle creation. |
|
|
""" |
|
|
try: |
|
|
words = await crossword_gen.generate_words_for_topics( |
|
|
topics=request.topics, |
|
|
difficulty=request.difficulty, |
|
|
use_ai=request.useAI |
|
|
) |
|
|
|
|
|
return { |
|
|
"topics": request.topics, |
|
|
"difficulty": request.difficulty, |
|
|
"useAI": request.useAI, |
|
|
"wordCount": len(words), |
|
|
"words": words |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Error generating words: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
@router.get("/health") |
|
|
async def api_health(): |
|
|
"""API health check.""" |
|
|
return { |
|
|
"status": "healthy", |
|
|
"timestamp": datetime.utcnow().isoformat(), |
|
|
"backend": "python", |
|
|
"version": "2.0.0" |
|
|
} |
|
|
|
|
|
@router.get("/debug/vector-search") |
|
|
async def debug_vector_search( |
|
|
topic: str, |
|
|
difficulty: str = "medium", |
|
|
max_words: int = 10, |
|
|
request: Request = None |
|
|
): |
|
|
""" |
|
|
Debug endpoint to test vector search directly. |
|
|
""" |
|
|
try: |
|
|
vector_service = getattr(request.app.state, 'vector_service', None) |
|
|
if not vector_service or not vector_service.is_initialized: |
|
|
raise HTTPException(status_code=503, detail="Vector search service not available") |
|
|
|
|
|
words = await vector_service.find_similar_words(topic, difficulty, max_words) |
|
|
|
|
|
return { |
|
|
"topic": topic, |
|
|
"difficulty": difficulty, |
|
|
"max_words": max_words, |
|
|
"found_words": len(words), |
|
|
"words": words |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"❌ Vector search debug failed: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |