Coverage for apps / ai / api_discover.py: 78%
49 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 10:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 10:49 +0000
1"""AI discover suggestions API endpoints."""
3import logging
4from typing import List
6from django.conf import settings
7from django_ratelimit.decorators import ratelimit
8from ninja import Router, Schema, Status
10from apps.core.auth import SessionAuth
11from apps.profiles.models import Profile
13from .api import ErrorOut, handle_ai_errors
14from .models import AIDiscoverySuggestion
15from .services.quota import release_quota, reserve_quota
16from .services.discover import get_discover_suggestions, CACHE_DURATION_HOURS
18security_logger = logging.getLogger("security")
20router = Router(tags=["ai"])
23# Schemas
26class DiscoverSuggestionOut(Schema):
27 type: str
28 title: str
29 description: str
30 search_query: str
33class DiscoverOut(Schema):
34 suggestions: List[DiscoverSuggestionOut]
35 refreshed_at: str
38# Endpoints
41@router.get(
42 "/discover/{profile_id}/", response={200: DiscoverOut, 404: ErrorOut, 429: dict, 503: ErrorOut}, auth=SessionAuth()
43)
44@ratelimit(key="ip", rate="20/h", method="GET", block=False)
45@handle_ai_errors
46def discover_endpoint(request, profile_id: int, refresh: bool = False):
47 """Get AI discovery suggestions for a profile.
49 Returns cached suggestions if still valid (within 24 hours),
50 otherwise generates new suggestions via AI.
52 For new users (no favorites), only seasonal suggestions are returned.
53 Pass ?refresh=true to force regeneration.
54 """
55 if getattr(request, "limited", False):
56 security_logger.warning("Rate limit hit: /ai/discover from %s", request.META.get("REMOTE_ADDR"))
57 return Status(429, {"error": "rate_limited", "message": "Too many requests. Please try again later."})
59 # In passkey mode, verify profile ownership
60 if settings.AUTH_MODE != "home":
61 session_profile_id = request.session.get("profile_id")
62 if session_profile_id != profile_id:
63 return Status(404, {"error": "not_found", "message": "Not found"})
65 try:
66 profile = Profile.objects.select_related("user").get(pk=profile_id)
67 except Profile.DoesNotExist:
68 return Status(404, {"error": "not_found", "message": f"Profile {profile_id} not found"})
70 # Only count against quota when OpenRouter is actually called (not cache hits)
71 from datetime import timedelta
73 from django.utils import timezone
75 cache_cutoff = timezone.now() - timedelta(hours=CACHE_DURATION_HOURS)
76 has_cached = (
77 not refresh and AIDiscoverySuggestion.objects.filter(profile=profile, created_at__gte=cache_cutoff).exists()
78 )
80 if not has_cached:
81 allowed, info = reserve_quota(profile, "discover")
82 if not allowed:
83 return Status(429, {"error": "quota_exceeded", "message": "Daily limit reached for discover", **info})
85 try:
86 result = get_discover_suggestions(profile_id, force_refresh=refresh)
87 return result
88 except Profile.DoesNotExist:
89 if not has_cached:
90 release_quota(profile, "discover")
91 return Status(
92 404,
93 {
94 "error": "not_found",
95 "message": f"Profile {profile_id} not found",
96 },
97 )
98 except Exception:
99 if not has_cached:
100 release_quota(profile, "discover")
101 raise