Spaces:
Build error
Build error
| from fastapi import APIRouter, HTTPException, Request | |
| from fastapi.responses import RedirectResponse | |
| from .schemas import RecommendRequest, RecommendResponse, FeedbackRequest, FeedbackResponse, StatsResponse | |
| import time | |
| from artwork_bandit.db import database | |
| import numpy as np | |
| router = APIRouter() | |
| def require_auth(request: Request): | |
| if getattr(request.app.state, 'sso_enabled', False) and not request.session.get('user'): | |
| raise HTTPException(status_code=401, detail='Authentication required') | |
| async def auth_user(request: Request): | |
| return { | |
| 'is_authenticated': bool(request.session.get('user')), | |
| 'user': request.session.get('user'), | |
| 'sso_enabled': getattr(request.app.state, 'sso_enabled', False), | |
| } | |
| async def auth_login(request: Request): | |
| if not getattr(request.app.state, 'sso_enabled', False): | |
| raise HTTPException(status_code=400, detail='SSO is not configured') | |
| redirect_uri = request.url_for('auth_callback') | |
| return await request.app.state.oauth.sso.authorize_redirect(request, redirect_uri) | |
| async def auth_callback(request: Request): | |
| if not getattr(request.app.state, 'sso_enabled', False): | |
| raise HTTPException(status_code=400, detail='SSO is not configured') | |
| token = await request.app.state.oauth.sso.authorize_access_token(request) | |
| user = token.get('userinfo') | |
| if not user: | |
| user = await request.app.state.oauth.sso.parse_id_token(request, token) | |
| request.session['user'] = { | |
| 'name': user.get('name') or user.get('email') or user.get('preferred_username'), | |
| 'email': user.get('email'), | |
| 'sub': user.get('sub'), | |
| } | |
| return RedirectResponse(request.app.state.frontend_url) | |
| async def auth_logout(request: Request): | |
| request.session.clear() | |
| return RedirectResponse(request.app.state.frontend_url) | |
| async def list_users(request: Request): | |
| require_auth(request) | |
| app = request.app | |
| users = getattr(app.state, 'users', None) | |
| if users is None: | |
| raise HTTPException(status_code=500, detail='users not loaded') | |
| # return compact list | |
| return [{'user_id': u.get('user_id'), 'age': u.get('age'), 'mood': u.get('mood')} for u in users] | |
| async def list_contents(request: Request): | |
| require_auth(request) | |
| app = request.app | |
| contents = getattr(app.state, 'contents', None) | |
| if contents is None: | |
| raise HTTPException(status_code=500, detail='contents not loaded') | |
| return [{'content_id': c.get('content_id'), 'title': c.get('title', ''), 'genre': c.get('genre')} for c in contents] | |
| async def recommend(req: RecommendRequest, request: Request): | |
| require_auth(request) | |
| app = request.app | |
| start = time.time() | |
| fs = app.state.feature_store | |
| if fs is None: | |
| raise HTTPException(status_code=500, detail='Feature store not initialised') | |
| # validate | |
| if fs.get_user_embedding(req.user_id) is None: | |
| raise HTTPException(status_code=404, detail='user_id not found') | |
| if fs.get_content_embedding(req.content_id) is None: | |
| raise HTTPException(status_code=404, detail='content_id not found') | |
| artworks = fs.get_artworks_for_content(req.content_id) | |
| if not artworks or len(artworks) < 1: | |
| raise HTTPException(status_code=404, detail='no artworks for content') | |
| # build contexts | |
| contexts = {} | |
| for aid in artworks: | |
| vec = fs.build_context_vector(req.user_id, req.content_id, aid) | |
| if vec is None: | |
| continue | |
| contexts[aid] = vec | |
| # select | |
| algorithm = req.algorithm.lower() | |
| if algorithm == 'linucb': | |
| bandit = app.state.linucb | |
| chosen = bandit.select(contexts) | |
| elif algorithm == 'thompson': | |
| bandit = app.state.thompson | |
| chosen = bandit.select(list(contexts.keys())) | |
| else: | |
| raise HTTPException(status_code=422, detail='unknown algorithm') | |
| impression_id = database.log_impression(req.user_id, req.content_id, chosen) | |
| latency_ms = (time.time() - start) * 1000.0 | |
| if latency_ms > 100: | |
| app.logger = getattr(app, 'logger', None) | |
| if app.logger: | |
| app.logger.warning(f"High latency: {latency_ms}ms") | |
| artwork = next((a for a in app.state.artworks if a.get('artwork_id') == chosen), None) | |
| if artwork is None or artwork.get('artwork_image') is None: | |
| raise HTTPException(status_code=500, detail='artwork image not found') | |
| return RecommendResponse( | |
| artwork_id=chosen, | |
| content_id=req.content_id, | |
| user_id=req.user_id, | |
| algorithm=algorithm, | |
| latency_ms=latency_ms, | |
| impression_id=impression_id, | |
| artwork_image=artwork['artwork_image'], | |
| ) | |
| async def feedback(req: FeedbackRequest, request: Request): | |
| require_auth(request) | |
| app = request.app | |
| imp = database.get_impression(req.impression_id) | |
| if imp is None: | |
| raise HTTPException(status_code=404, detail='impression not found') | |
| if req.reward not in (0.0, 1.0): | |
| raise HTTPException(status_code=422, detail='reward must be 0.0 or 1.0') | |
| database.log_reward(req.impression_id, req.reward) | |
| fs = app.state.feature_store | |
| ctx = fs.build_context_vector(imp.user_id, imp.content_id, imp.artwork_id) | |
| if ctx is None: | |
| raise HTTPException(status_code=500, detail='context not found') | |
| # update both bandits | |
| if hasattr(app.state, 'linucb'): | |
| app.state.linucb.update(imp.artwork_id, ctx, req.reward) | |
| if hasattr(app.state, 'thompson'): | |
| app.state.thompson.update(imp.artwork_id, req.reward) | |
| return FeedbackResponse(status='ok', impression_id=req.impression_id, updated_arm=imp.artwork_id) | |
| async def stats(request: Request): | |
| require_auth(request) | |
| rows = database.impressions_with_rewards() | |
| total = len(rows) | |
| clicks = sum(1 for r in rows if r.reward and r.reward > 0) | |
| impressions_by = {} | |
| clicks_by = {} | |
| for r in rows: | |
| impressions_by[r.artwork_id] = impressions_by.get(r.artwork_id, 0) + 1 | |
| if r.reward and r.reward > 0: | |
| clicks_by[r.artwork_id] = clicks_by.get(r.artwork_id, 0) + 1 | |
| overall_ctr = float(clicks) / total if total > 0 else 0.0 | |
| return StatsResponse(total_impressions=total, total_clicks=clicks, overall_ctr=overall_ctr, impressions_by_artwork=impressions_by, clicks_by_artwork=clicks_by) | |