Fix: Persona Polling Issue
This commit is contained in:
@@ -37,7 +37,7 @@ class OnboardingCompletionService:
|
|||||||
# Validate API keys are configured
|
# Validate API keys are configured
|
||||||
self._validate_api_keys()
|
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)
|
persona_generated = await self._generate_persona_from_onboarding(user_id)
|
||||||
|
|
||||||
# Complete the onboarding process
|
# Complete the onboarding process
|
||||||
@@ -144,9 +144,18 @@ class OnboardingCompletionService:
|
|||||||
try:
|
try:
|
||||||
persona_service = PersonaAnalysisService()
|
persona_service = PersonaAnalysisService()
|
||||||
|
|
||||||
# Use user_id = 1 for now (assuming single user system)
|
# If a persona already exists for this user, skip regeneration
|
||||||
persona_user_id = 1
|
try:
|
||||||
persona_result = persona_service.generate_persona_from_onboarding(persona_user_id)
|
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:
|
if "error" not in persona_result:
|
||||||
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
|
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
|
||||||
|
|||||||
@@ -531,7 +531,13 @@ async def execute_persona_generation_task(task_id: str, persona_request: Persona
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "error" in core_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
|
return
|
||||||
|
|
||||||
update_task_status(task_id, "running", 40, "Core persona generated successfully")
|
update_task_status(task_id, "running", 40, "Core persona generated successfully")
|
||||||
|
|||||||
@@ -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}")
|
logger.error(f"API key error in Gemini Pro structured JSON generation: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
except Exception as 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)
|
msg = str(e)
|
||||||
if "RESOURCE_EXHAUSTED" in msg or "429" in msg or "rate limit" in msg.lower():
|
if "RESOURCE_EXHAUSTED" in msg or "429" in msg or "quota" in msg.lower():
|
||||||
# If RetryInfo is present with a retryDelay, honor it before re-raising
|
logger.error(f"Rate limit/quota error in Gemini Pro structured JSON generation: {msg}")
|
||||||
try:
|
# Return error instead of retrying - quota exhausted means we need to wait or upgrade plan
|
||||||
import re, time
|
return {"error": msg}
|
||||||
m = re.search(r"retryDelay':\s*'?(\d+)s" , msg)
|
# For other errors, let tenacity handle retries
|
||||||
if m:
|
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
||||||
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
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1 B After Width: | Height: | Size: 15 KiB |
@@ -19,7 +19,8 @@ export interface PersonaTaskStatus {
|
|||||||
|
|
||||||
export interface UsePersonaPollingOptions {
|
export interface UsePersonaPollingOptions {
|
||||||
interval?: number; // Polling interval in milliseconds
|
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
|
onProgress?: (message: string, progress: number) => void; // Callback for progress updates
|
||||||
onComplete?: (result: any) => void; // Callback when task completes
|
onComplete?: (result: any) => void; // Callback when task completes
|
||||||
onError?: (error: string) => void; // Callback when task fails
|
onError?: (error: string) => void; // Callback when task fails
|
||||||
@@ -40,6 +41,8 @@ export interface UsePersonaPollingReturn {
|
|||||||
export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePersonaPollingReturn {
|
export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePersonaPollingReturn {
|
||||||
const {
|
const {
|
||||||
interval = 2000, // 2 seconds default
|
interval = 2000, // 2 seconds default
|
||||||
|
maxAttempts = 180, // 6 minutes at 2s interval
|
||||||
|
maxDuration = 600000, // 10 minutes in milliseconds
|
||||||
onProgress,
|
onProgress,
|
||||||
onComplete,
|
onComplete,
|
||||||
onError
|
onError
|
||||||
@@ -67,6 +70,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const attemptsRef = useRef(0);
|
const attemptsRef = useRef(0);
|
||||||
const currentTaskIdRef = useRef<string | null>(null);
|
const currentTaskIdRef = useRef<string | null>(null);
|
||||||
|
const startTimeRef = useRef<number>(0);
|
||||||
|
const stuckProgressRef = useRef<number>(0);
|
||||||
|
const stuckCountRef = useRef<number>(0);
|
||||||
|
|
||||||
const stopPolling = useCallback(() => {
|
const stopPolling = useCallback(() => {
|
||||||
console.log('stopPersonaPolling called');
|
console.log('stopPersonaPolling called');
|
||||||
@@ -78,6 +84,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
setIsPolling(false);
|
setIsPolling(false);
|
||||||
attemptsRef.current = 0;
|
attemptsRef.current = 0;
|
||||||
currentTaskIdRef.current = null;
|
currentTaskIdRef.current = null;
|
||||||
|
startTimeRef.current = 0;
|
||||||
|
stuckProgressRef.current = 0;
|
||||||
|
stuckCountRef.current = 0;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startPolling = useCallback((taskId: string) => {
|
const startPolling = useCallback((taskId: string) => {
|
||||||
@@ -97,6 +106,9 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
attemptsRef.current = 0;
|
attemptsRef.current = 0;
|
||||||
|
startTimeRef.current = Date.now();
|
||||||
|
stuckProgressRef.current = 0;
|
||||||
|
stuckCountRef.current = 0;
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
if (!currentTaskIdRef.current) {
|
if (!currentTaskIdRef.current) {
|
||||||
@@ -104,6 +116,25 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
return;
|
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 {
|
try {
|
||||||
const response = await apiClient.get(`/api/onboarding/step4/persona-task/${currentTaskIdRef.current}`);
|
const response = await apiClient.get(`/api/onboarding/step4/persona-task/${currentTaskIdRef.current}`);
|
||||||
const status: PersonaTaskStatus = response.data;
|
const status: PersonaTaskStatus = response.data;
|
||||||
@@ -113,6 +144,21 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
setProgress(status.progress);
|
setProgress(status.progress);
|
||||||
setCurrentStep(status.current_step);
|
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
|
// Update progress messages
|
||||||
if (status.progress_messages && status.progress_messages.length > 0) {
|
if (status.progress_messages && status.progress_messages.length > 0) {
|
||||||
console.log('Progress messages received:', status.progress_messages);
|
console.log('Progress messages received:', status.progress_messages);
|
||||||
@@ -154,7 +200,7 @@ export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePe
|
|||||||
// Start polling immediately, then at intervals
|
// Start polling immediately, then at intervals
|
||||||
poll();
|
poll();
|
||||||
intervalRef.current = setInterval(poll, interval);
|
intervalRef.current = setInterval(poll, interval);
|
||||||
}, [isPolling, interval, onProgress, onComplete, onError, stopPolling]);
|
}, [isPolling, interval, maxAttempts, maxDuration, onProgress, onComplete, onError, stopPolling]);
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user