Files

175 lines
5.8 KiB
Python

"""Ingest module router for image analysis."""
import json
import asyncio
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from app.modules.ingest.schemas import AnalyzeRequest, AnalyzeResponse, AnalyzeError
from app.modules.ingest.service import analyze_documents
from app.modules.auth.service import current_active_user
from typing import Optional
from app.modules.auth.models import User, UserRole
from app.core.db import get_async_session
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.config import settings
router = APIRouter()
async def get_effective_api_key(user: User, db: AsyncSession) -> Optional[str]:
"""Retrieve the API key from user, parent, or global settings."""
# 1. User specific key
if user.openrouter_api_key:
return user.openrouter_api_key
# 2. Parent's key (if learner)
if user.parent_id:
statement = select(User).where(User.id == user.parent_id)
result = await db.execute(statement)
parent = result.scalar_one_or_none()
if parent and parent.openrouter_api_key:
return parent.openrouter_api_key
# 3. Global settings key
if settings.OPENROUTER_API_KEY:
return settings.OPENROUTER_API_KEY
return None
@router.post(
"/analyze",
response_model=AnalyzeResponse,
responses={
400: {"model": AnalyzeError, "description": "Invalid request"},
401: {"model": AnalyzeError, "description": "No API key configured"},
500: {"model": AnalyzeError, "description": "AI service error"}
}
)
async def analyze_image_endpoint(
request: AnalyzeRequest,
user: User = Depends(current_active_user),
db: AsyncSession = Depends(get_async_session)
):
"""
Analyze an image using AI Vision to extract lesson content.
Requires a valid OpenRouter API key configured in user settings.
"""
# Determine which API key to use
api_key = await get_effective_api_key(user, db)
if not api_key:
raise HTTPException(
status_code=401,
detail="No OpenRouter API key configured. Please add your API key in Settings or contact your administrator."
)
if not request.images_base64:
raise HTTPException(
status_code=400,
detail="No images provided"
)
try:
result, math_safety_triggered = await analyze_documents(
request.images_base64,
api_key
)
# Update usage
usage = result.get("usage", {})
user.total_tokens_used += usage.get("total_tokens", 0)
# Approximate cost (0.1$ / 1M tokens for Flash)
user.total_cost_usd += usage.get("total_tokens", 0) * 0.0000001
db.add(user)
await db.commit()
return AnalyzeResponse(**result)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"AI analysis failed: {str(e)}"
)
@router.post("/analyze-stream")
async def analyze_image_stream(
request: AnalyzeRequest,
user: User = Depends(current_active_user),
db: AsyncSession = Depends(get_async_session)
):
"""
Analyze an image with Server-Sent Events for progress updates.
Streams progress events: uploading -> reading -> analyzing -> complete
"""
# Determine which API key to use
api_key = await get_effective_api_key(user, db)
if not api_key:
raise HTTPException(
status_code=401,
detail="No OpenRouter API key configured. Please add your API key in Settings or contact your administrator."
)
if not request.images_base64:
raise HTTPException(
status_code=400,
detail="No images provided"
)
async def event_generator():
try:
# Step 1: Uploading
yield f"data: {json.dumps({'step': 'uploading', 'message': 'Téléchargement...', 'progress': 10})}\n\n"
await asyncio.sleep(0.1)
# Step 2: Reading (OCR)
msg = {'step': 'reading', 'message': "Lecture des documents...", 'progress': 30}
yield f"data: {json.dumps(msg)}\n\n"
await asyncio.sleep(0.1)
# Step 3: Analyzing
yield f"data: {json.dumps({'step': 'analyzing', 'message': 'Analyse IA en cours...', 'progress': 50})}\n\n"
# Actually perform the analysis
result, math_safety_triggered = await analyze_documents(
request.images_base64,
api_key
)
# Update usage
usage = result.get("usage", {})
user.total_tokens_used += usage.get("total_tokens", 0)
user.total_cost_usd += usage.get("total_tokens", 0) * 0.0000001
db.add(user)
await db.commit()
# Step 4: Synthesizing
yield f"data: {json.dumps({'step': 'synthesizing', 'message': 'Génération de la synthèse...', 'progress': 80})}\n\n"
await asyncio.sleep(0.1)
# Step 5: Complete
yield f"data: {json.dumps({'step': 'complete', 'message': 'Terminé!', 'progress': 100, 'result': result})}\n\n"
except Exception as e:
import traceback
error_msg = f"Error in analyze_image_stream: {str(e)}\n{traceback.format_exc()}"
print(error_msg) # Log to console
yield f"data: {json.dumps({'step': 'error', 'message': str(e), 'progress': 0})}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)