diff --git a/backend/api/onboarding_utils/onboarding_completion_service.py b/backend/api/onboarding_utils/onboarding_completion_service.py index d149b491..a2f01b7c 100644 --- a/backend/api/onboarding_utils/onboarding_completion_service.py +++ b/backend/api/onboarding_utils/onboarding_completion_service.py @@ -37,7 +37,7 @@ class OnboardingCompletionService: # Validate API keys are configured self._validate_api_keys() - # Generate writing persona from onboarding data + # Generate writing persona from onboarding data only if not already present persona_generated = await self._generate_persona_from_onboarding(user_id) # Complete the onboarding process @@ -144,9 +144,18 @@ class OnboardingCompletionService: try: persona_service = PersonaAnalysisService() - # Use user_id = 1 for now (assuming single user system) - persona_user_id = 1 - persona_result = persona_service.generate_persona_from_onboarding(persona_user_id) + # If a persona already exists for this user, skip regeneration + try: + existing = persona_service.get_user_personas(int(user_id)) + if existing and len(existing) > 0: + logger.info("Persona already exists for user %s; skipping regeneration during completion", user_id) + return False + except Exception: + # Non-fatal; proceed to attempt generation + pass + + # Generate persona for this user + persona_result = persona_service.generate_persona_from_onboarding(int(user_id)) if "error" not in persona_result: logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}") diff --git a/backend/api/onboarding_utils/step4_persona_routes.py b/backend/api/onboarding_utils/step4_persona_routes.py index fa35a26a..dec2871d 100644 --- a/backend/api/onboarding_utils/step4_persona_routes.py +++ b/backend/api/onboarding_utils/step4_persona_routes.py @@ -531,7 +531,13 @@ async def execute_persona_generation_task(task_id: str, persona_request: Persona ) if "error" in core_persona: - update_task_status(task_id, "failed", 0, f"Core persona generation failed: {core_persona['error']}") + error_msg = core_persona['error'] + # Check if this is a quota/rate limit error + if "RESOURCE_EXHAUSTED" in str(error_msg) or "429" in str(error_msg) or "quota" in str(error_msg).lower(): + update_task_status(task_id, "failed", 0, f"Quota exhausted: {error_msg}", error=str(error_msg)) + logger.error(f"Task {task_id}: Quota exhausted, marking as failed immediately") + else: + update_task_status(task_id, "failed", 0, f"Core persona generation failed: {error_msg}", error=str(error_msg)) return update_task_status(task_id, "running", 40, "Core persona generated successfully") diff --git a/backend/services/llm_providers/gemini_provider.py b/backend/services/llm_providers/gemini_provider.py index 777a5057..b20cb695 100644 --- a/backend/services/llm_providers/gemini_provider.py +++ b/backend/services/llm_providers/gemini_provider.py @@ -475,20 +475,14 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9, logger.error(f"API key error in Gemini Pro structured JSON generation: {e}") return {"error": str(e)} except Exception as e: - # Let tenacity handle retries, especially for 429 RESOURCE_EXHAUSTED + # Check if this is a quota/rate limit error msg = str(e) - if "RESOURCE_EXHAUSTED" in msg or "429" in msg or "rate limit" in msg.lower(): - # If RetryInfo is present with a retryDelay, honor it before re-raising - try: - import re, time - m = re.search(r"retryDelay':\s*'?(\d+)s" , msg) - if m: - delay_s = int(m.group(1)) - logger.warning(f"Rate limit hit, sleeping {delay_s}s before retry...") - time.sleep(delay_s) - except Exception: - pass - # Re-raise to trigger tenacity's backoff/retry + if "RESOURCE_EXHAUSTED" in msg or "429" in msg or "quota" in msg.lower(): + logger.error(f"Rate limit/quota error in Gemini Pro structured JSON generation: {msg}") + # Return error instead of retrying - quota exhausted means we need to wait or upgrade plan + return {"error": msg} + # For other errors, let tenacity handle retries + logger.error(f"Error in Gemini Pro structured JSON generation: {e}") raise diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 8b137891..868a7b8b 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/src/hooks/usePersonaPolling.ts b/frontend/src/hooks/usePersonaPolling.ts index 3d479dd3..92fb44fe 100644 --- a/frontend/src/hooks/usePersonaPolling.ts +++ b/frontend/src/hooks/usePersonaPolling.ts @@ -19,7 +19,8 @@ export interface PersonaTaskStatus { export interface UsePersonaPollingOptions { interval?: number; // Polling interval in milliseconds - maxAttempts?: number; // Maximum number of polling attempts + maxAttempts?: number; // Maximum number of polling attempts (default: 180 = 6 minutes at 2s interval) + maxDuration?: number; // Maximum polling duration in milliseconds (default: 10 minutes) onProgress?: (message: string, progress: number) => void; // Callback for progress updates onComplete?: (result: any) => void; // Callback when task completes onError?: (error: string) => void; // Callback when task fails @@ -40,6 +41,8 @@ export interface UsePersonaPollingReturn { export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePersonaPollingReturn { const { interval = 2000, // 2 seconds default + maxAttempts = 180, // 6 minutes at 2s interval + maxDuration = 600000, // 10 minutes in milliseconds onProgress, onComplete, onError @@ -67,6 +70,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe const intervalRef = useRef(null); const attemptsRef = useRef(0); const currentTaskIdRef = useRef(null); + const startTimeRef = useRef(0); + const stuckProgressRef = useRef(0); + const stuckCountRef = useRef(0); const stopPolling = useCallback(() => { console.log('stopPersonaPolling called'); @@ -78,6 +84,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe setIsPolling(false); attemptsRef.current = 0; currentTaskIdRef.current = null; + startTimeRef.current = 0; + stuckProgressRef.current = 0; + stuckCountRef.current = 0; }, []); const startPolling = useCallback((taskId: string) => { @@ -97,6 +106,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe setResult(null); setError(null); attemptsRef.current = 0; + startTimeRef.current = Date.now(); + stuckProgressRef.current = 0; + stuckCountRef.current = 0; const poll = async () => { if (!currentTaskIdRef.current) { @@ -104,6 +116,25 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe return; } + // Check max attempts + if (attemptsRef.current >= maxAttempts) { + console.error('Persona polling: Max attempts reached'); + setError('Persona generation timed out - please try again later'); + onError?.('Persona generation timed out after maximum attempts'); + stopPolling(); + return; + } + + // Check max duration + const elapsed = Date.now() - startTimeRef.current; + if (elapsed >= maxDuration) { + console.error('Persona polling: Max duration reached'); + setError('Persona generation timed out - please try again later'); + onError?.('Persona generation exceeded maximum duration'); + stopPolling(); + return; + } + try { const response = await apiClient.get(`/api/onboarding/step4/persona-task/${currentTaskIdRef.current}`); const status: PersonaTaskStatus = response.data; @@ -113,6 +144,21 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe setProgress(status.progress); setCurrentStep(status.current_step); + // Detect stuck progress (same progress for 20+ consecutive polls = ~40 seconds) + if (status.progress === stuckProgressRef.current) { + stuckCountRef.current++; + if (stuckCountRef.current >= 20) { + console.error('Persona polling: Progress stuck at', status.progress, 'for too long'); + setError('Persona generation appears stuck - please try again or contact support'); + onError?.('Persona generation stuck - no progress for extended period'); + stopPolling(); + return; + } + } else { + stuckProgressRef.current = status.progress; + stuckCountRef.current = 0; + } + // Update progress messages if (status.progress_messages && status.progress_messages.length > 0) { console.log('Progress messages received:', status.progress_messages); @@ -154,7 +200,7 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe // Start polling immediately, then at intervals poll(); intervalRef.current = setInterval(poll, interval); - }, [isPolling, interval, onProgress, onComplete, onError, stopPolling]); + }, [isPolling, interval, maxAttempts, maxDuration, onProgress, onComplete, onError, stopPolling]); // Cleanup on unmount useEffect(() => {