From 08ce9588f48c96cafc4cf7c6815546e5d36bfbb6 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Sun, 12 Oct 2025 14:27:15 +0530 Subject: [PATCH] Fix: Persona Polling Issue --- .../onboarding_completion_service.py | 17 ++++-- .../onboarding_utils/step4_persona_routes.py | 8 ++- .../services/llm_providers/gemini_provider.py | 20 +++---- frontend/public/favicon.ico | Bin 1 -> 15406 bytes frontend/src/hooks/usePersonaPolling.ts | 50 +++++++++++++++++- 5 files changed, 75 insertions(+), 20 deletions(-) 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 8b137891791fe96927ad78e64b0aad7bded08bdc..868a7b8b1caf2cb3f44d820bff0086948c28857d 100644 GIT binary patch literal 15406 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 literal 1 Icmd-A000XB3jhEB 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(() => {