import uuid from datetime import datetime, timezone from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession import anthropic import deepl from ..auth import verify_token from ..database import get_db, AsyncSessionLocal from ..models import Job from ..config import settings router = APIRouter(prefix="/generate", tags=["generation"]) SUPPORTED_LANGUAGES: dict[str, str] = { "en": "English", "fr": "French", "es": "Spanish", "it": "Italian", "de": "German", } SUPPORTED_LEVELS = {"A1", "A2", "B1", "B2", "C1", "C2"} # Maps our language codes to DeepL source/target language codes DEEPL_SOURCE_LANG: dict[str, str] = { "en": "EN", "fr": "FR", "es": "ES", "it": "IT", "de": "DE", } # DeepL target codes (English needs a regional variant) DEEPL_TARGET_LANG: dict[str, str] = { "en": "EN-US", "fr": "FR", "es": "ES", "it": "IT", "de": "DE", } class GenerationRequest(BaseModel): target_language: str complexity_level: str input_texts: list[str] topic: str | None = None source_language: str = "en" class GenerationResponse(BaseModel): job_id: str async def _run_generation(job_id: uuid.UUID, request: GenerationRequest) -> None: async with AsyncSessionLocal() as db: job = await db.get(Job, job_id) job.status = "processing" job.started_at = datetime.now(timezone.utc) await db.commit() try: from_language = SUPPORTED_LANGUAGES[request.source_language] language_name = SUPPORTED_LANGUAGES[request.target_language] # Build a short summary of the input to store (not the full text) topic_part = f"Topic: {request.topic}. " if request.topic else "" combined_preview = " ".join(request.input_texts)[:300] input_summary = ( f"{topic_part}Based on {len(request.input_texts)} input text(s): " f"{combined_preview}..." ) source_material = "\n\n".join(request.input_texts[:3]) topic_line = f"\nTopic focus: {request.topic}" if request.topic else "" prompt = ( f"You are a language learning content creator. " f"Using the input provided, you generate engaging realistic text in {language_name} " f"at {request.complexity_level} proficiency level (CEFR scale).\n\n" f"The text should:\n" f"- Be appropriate for a {request.complexity_level} learner\n" f"- Maintain a similar tone to the input text. Where appropriate, use idioms\n" f"- Feel natural and authentic, like content a native speaker would read\n" f"- Be formatted in markdown with paragraphs and line breaks\n" f"- Be 200–400 words long\n" f"- Be inspired by the following source material " f"(but written originally in {language_name}):\n\n" f"{source_material}" f"{topic_line}\n\n" f"Respond with ONLY the generated text in {language_name}, " f"no explanations or translations.\n" f"The 'Topic focus' should be a comma-separated list of up to three topics, in {language_name}." ) client = anthropic.Anthropic(api_key=settings.anthropic_api_key) message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[{"role": "user", "content": prompt}], ) generated_text = message.content[0].text # TODO: Come back to this when DeepL unblock my account for being "high risk" # Translate generated text back into the learner's source language via DeepL # translator = deepl.Translator(settings.deepl_api_key) # translation = translator.translate_text( # generated_text, # source_lang=DEEPL_SOURCE_LANG[request.target_language], # target_lang=DEEPL_TARGET_LANG[request.source_language], #) translate_prompt = ( f"You are a helpful assistant that translates text. Translate just the previous summary " f"content in {language_name} text you generated based on the input I gave you. Translate " f"it back into {from_language}.\n" f"- Keep the translation as close as possible to the original meaning and tone\n" f"- Send through only the translated text, no explanations or notes\n" ) translate_message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[ { "role": "user", "content": prompt }, { "role": "assistant", "content": message.content }, { "role": "user", "content": translate_prompt } ], ) job.status = "succeeded" job.generated_text = generated_text job.translated_text = translate_message.content[0].text job.input_summary = input_summary[:500] job.completed_at = datetime.now(timezone.utc) except Exception as exc: job.status = "failed" job.error_message = str(exc) job.completed_at = datetime.now(timezone.utc) await db.commit() @router.post("", response_model=GenerationResponse, status_code=202) async def create_generation_job( request: GenerationRequest, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), _: dict = Depends(verify_token), ) -> GenerationResponse: if request.target_language not in SUPPORTED_LANGUAGES: raise HTTPException( status_code=400, detail=f"Unsupported language '{request.target_language}'. " f"Supported: {list(SUPPORTED_LANGUAGES)}", ) if request.complexity_level not in SUPPORTED_LEVELS: raise HTTPException( status_code=400, detail=f"Unsupported level '{request.complexity_level}'. " f"Supported: {sorted(SUPPORTED_LEVELS)}", ) job = Job( source_language=request.source_language, target_language=request.target_language, complexity_level=request.complexity_level, ) db.add(job) await db.commit() await db.refresh(job) background_tasks.add_task(_run_generation, job.id, request) return GenerationResponse(job_id=str(job.id))