Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
500 lines
22 KiB
Python
500 lines
22 KiB
Python
"""
|
|
Onboarding Manager Module
|
|
Handles all onboarding-related endpoints and functionality.
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
|
|
from fastapi.responses import FileResponse
|
|
from typing import Dict, Any, Optional
|
|
from loguru import logger
|
|
|
|
# Import onboarding functions
|
|
from api.onboarding import (
|
|
health_check,
|
|
initialize_onboarding,
|
|
get_onboarding_status,
|
|
get_onboarding_progress_full,
|
|
get_step_data,
|
|
complete_step,
|
|
skip_step,
|
|
validate_step_access,
|
|
get_api_keys,
|
|
get_api_keys_for_onboarding,
|
|
save_api_key,
|
|
validate_api_keys,
|
|
start_onboarding,
|
|
complete_onboarding,
|
|
reset_onboarding,
|
|
get_resume_info,
|
|
get_onboarding_config,
|
|
get_provider_setup_info,
|
|
get_all_providers_info,
|
|
validate_provider_key,
|
|
get_enhanced_validation_status,
|
|
get_onboarding_summary,
|
|
get_website_analysis_data,
|
|
get_research_preferences_data,
|
|
save_business_info,
|
|
get_business_info,
|
|
get_business_info_by_user,
|
|
update_business_info,
|
|
generate_writing_personas,
|
|
generate_writing_personas_async,
|
|
get_persona_task_status,
|
|
assess_persona_quality,
|
|
regenerate_persona,
|
|
get_persona_generation_options,
|
|
get_latest_persona,
|
|
save_persona_update,
|
|
StepCompletionRequest,
|
|
APIKeyRequest
|
|
)
|
|
from middleware.auth_middleware import get_current_user
|
|
|
|
|
|
class OnboardingManager:
|
|
"""Manages all onboarding-related endpoints and functionality."""
|
|
|
|
def __init__(self, app: FastAPI):
|
|
self.app = app
|
|
self.setup_onboarding_endpoints()
|
|
|
|
def setup_onboarding_endpoints(self):
|
|
"""Set up all onboarding-related endpoints."""
|
|
|
|
# Onboarding initialization - BATCH ENDPOINT (reduces 4 API calls to 1)
|
|
@self.app.get("/api/onboarding/init")
|
|
async def onboarding_init(current_user: dict = Depends(get_current_user)):
|
|
"""
|
|
Batch initialization endpoint - combines user info, status, and progress.
|
|
This eliminates 3-4 separate API calls on initial load, reducing latency by 60-75%.
|
|
"""
|
|
try:
|
|
return await initialize_onboarding(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_init: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Onboarding status endpoints
|
|
@self.app.get("/api/onboarding/status")
|
|
async def onboarding_status(current_user: dict = Depends(get_current_user)):
|
|
"""Get the current onboarding status."""
|
|
try:
|
|
return await get_onboarding_status(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_status: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/progress")
|
|
async def onboarding_progress(current_user: dict = Depends(get_current_user)):
|
|
"""Get the full onboarding progress data."""
|
|
try:
|
|
return await get_onboarding_progress_full(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_progress: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Step management endpoints
|
|
@self.app.get("/api/onboarding/step/{step_number}")
|
|
async def step_data(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
"""Get data for a specific step."""
|
|
try:
|
|
return await get_step_data(step_number, current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in step_data: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step/{step_number}/complete")
|
|
async def step_complete(step_number: int, request: StepCompletionRequest, current_user: dict = Depends(get_current_user)):
|
|
"""Mark a step as completed."""
|
|
try:
|
|
return await complete_step(step_number, request, current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in step_complete: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step/{step_number}/skip")
|
|
async def step_skip(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
"""Skip a step (for optional steps)."""
|
|
try:
|
|
return await skip_step(step_number, current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in step_skip: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/step/{step_number}/validate")
|
|
async def step_validate(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
"""Validate if user can access a specific step."""
|
|
try:
|
|
return await validate_step_access(step_number, current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in step_validate: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# API key management endpoints
|
|
@self.app.get("/api/onboarding/api-keys")
|
|
async def api_keys():
|
|
"""Get all configured API keys (masked)."""
|
|
try:
|
|
return await get_api_keys()
|
|
except Exception as e:
|
|
logger.error(f"Error in api_keys: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/api-keys/onboarding")
|
|
async def api_keys_for_onboarding(current_user: dict = Depends(get_current_user)):
|
|
"""Get all configured API keys for onboarding (unmasked)."""
|
|
try:
|
|
return await get_api_keys_for_onboarding(current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in api_keys_for_onboarding: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/api-keys")
|
|
async def api_key_save(request: APIKeyRequest, current_user: dict = Depends(get_current_user)):
|
|
"""Save an API key for a provider."""
|
|
try:
|
|
return await save_api_key(request, current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in api_key_save: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/api-keys/validate")
|
|
async def api_key_validate():
|
|
"""Get API key validation status and configuration."""
|
|
try:
|
|
import os
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
env_path = os.path.join(backend_dir, ".env")
|
|
load_dotenv(env_path, override=True)
|
|
|
|
# Check for required API keys (backend only)
|
|
api_keys = {}
|
|
required_keys = {
|
|
'GEMINI_API_KEY': 'gemini',
|
|
'EXA_API_KEY': 'exa'
|
|
# Note: CopilotKit is frontend-only, validated separately
|
|
}
|
|
|
|
missing_keys = []
|
|
configured_providers = []
|
|
|
|
for env_var, provider in required_keys.items():
|
|
key_value = os.getenv(env_var)
|
|
if key_value and key_value.strip():
|
|
api_keys[provider] = key_value.strip()
|
|
configured_providers.append(provider)
|
|
else:
|
|
missing_keys.append(provider)
|
|
|
|
# Determine if all required keys are present
|
|
required_providers = ['gemini', 'exa'] # Backend keys only
|
|
all_required_present = all(provider in configured_providers for provider in required_providers)
|
|
|
|
result = {
|
|
"api_keys": api_keys,
|
|
"validation_results": {
|
|
"gemini": {"valid": 'gemini' in configured_providers, "status": "configured" if 'gemini' in configured_providers else "missing"},
|
|
"exa": {"valid": 'exa' in configured_providers, "status": "configured" if 'exa' in configured_providers else "missing"}
|
|
},
|
|
"all_valid": all_required_present,
|
|
"total_providers": len(configured_providers),
|
|
"configured_providers": configured_providers,
|
|
"missing_keys": missing_keys
|
|
}
|
|
|
|
logger.info(f"API Key Validation Result: {result}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error in api_key_validate: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Onboarding control endpoints
|
|
@self.app.post("/api/onboarding/start")
|
|
async def onboarding_start(current_user: dict = Depends(get_current_user)):
|
|
"""Start a new onboarding session."""
|
|
try:
|
|
return await start_onboarding(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_start: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/complete")
|
|
async def onboarding_complete(current_user: dict = Depends(get_current_user)):
|
|
"""Complete the onboarding process."""
|
|
try:
|
|
return await complete_onboarding(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_complete: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/reset")
|
|
async def onboarding_reset():
|
|
"""Reset the onboarding progress."""
|
|
try:
|
|
return await reset_onboarding()
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_reset: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Resume functionality
|
|
@self.app.get("/api/onboarding/resume")
|
|
async def onboarding_resume():
|
|
"""Get information for resuming onboarding."""
|
|
try:
|
|
return await get_resume_info()
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_resume: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Configuration endpoints
|
|
@self.app.get("/api/onboarding/config")
|
|
async def onboarding_config():
|
|
"""Get onboarding configuration and requirements."""
|
|
try:
|
|
return get_onboarding_config()
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_config: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Enhanced provider endpoints
|
|
@self.app.get("/api/onboarding/providers/{provider}/setup")
|
|
async def provider_setup_info(provider: str):
|
|
"""Get setup information for a specific provider."""
|
|
try:
|
|
return await get_provider_setup_info(provider)
|
|
except Exception as e:
|
|
logger.error(f"Error in provider_setup_info: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/providers")
|
|
async def all_providers_info():
|
|
"""Get setup information for all providers."""
|
|
try:
|
|
return await get_all_providers_info()
|
|
except Exception as e:
|
|
logger.error(f"Error in all_providers_info: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/providers/{provider}/validate")
|
|
async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest):
|
|
"""Validate a specific provider's API key."""
|
|
try:
|
|
return await validate_provider_key(provider, request)
|
|
except Exception as e:
|
|
logger.error(f"Error in validate_provider_key: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/validation/enhanced")
|
|
async def enhanced_validation_status():
|
|
"""Get enhanced validation status for all configured services."""
|
|
try:
|
|
return await get_enhanced_validation_status()
|
|
except Exception as e:
|
|
logger.error(f"Error in enhanced_validation_status: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# New endpoints for FinalStep data loading
|
|
@self.app.get("/api/onboarding/summary")
|
|
async def onboarding_summary(current_user: dict = Depends(get_current_user)):
|
|
"""Get comprehensive onboarding summary for FinalStep."""
|
|
try:
|
|
return await get_onboarding_summary(current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in onboarding_summary: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/website-analysis")
|
|
async def website_analysis_data(current_user: dict = Depends(get_current_user)):
|
|
"""Get website analysis data for FinalStep."""
|
|
try:
|
|
return await get_website_analysis_data(current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in website_analysis_data: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/research-preferences")
|
|
async def research_preferences_data(current_user: dict = Depends(get_current_user)):
|
|
"""Get research preferences data for FinalStep."""
|
|
try:
|
|
return await get_research_preferences_data(current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in research_preferences_data: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Business Information endpoints
|
|
@self.app.post("/api/onboarding/business-info")
|
|
async def business_info_save(request: dict):
|
|
"""Save business information for users without websites."""
|
|
try:
|
|
from models.business_info_request import BusinessInfoRequest
|
|
return await save_business_info(request)
|
|
except Exception as e:
|
|
logger.error(f"Error in business_info_save: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/business-info/{business_info_id}")
|
|
async def business_info_get(business_info_id: int):
|
|
"""Get business information by ID."""
|
|
try:
|
|
return await get_business_info(business_info_id)
|
|
except Exception as e:
|
|
logger.error(f"Error in business_info_get: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/business-info/user/{user_id}")
|
|
async def business_info_get_by_user(user_id: int):
|
|
"""Get business information by user ID."""
|
|
try:
|
|
return await get_business_info_by_user(user_id)
|
|
except Exception as e:
|
|
logger.error(f"Error in business_info_get_by_user: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.put("/api/onboarding/business-info/{business_info_id}")
|
|
async def business_info_update(business_info_id: int, request: dict):
|
|
"""Update business information."""
|
|
try:
|
|
from models.business_info_request import BusinessInfoRequest
|
|
return await update_business_info(business_info_id, request)
|
|
except Exception as e:
|
|
logger.error(f"Error in business_info_update: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
# Persona generation endpoints
|
|
@self.app.post("/api/onboarding/step4/generate-personas")
|
|
async def generate_personas(request: dict, current_user: dict = Depends(get_current_user)):
|
|
"""Generate AI writing personas for Step 4."""
|
|
try:
|
|
return await generate_writing_personas(request, current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in generate_personas: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step4/generate-personas-async")
|
|
async def generate_personas_async(request: dict, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
|
|
"""Start async persona generation task."""
|
|
try:
|
|
return await generate_writing_personas_async(request, current_user, background_tasks)
|
|
except Exception as e:
|
|
logger.error(f"Error in generate_personas_async: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/step4/persona-task/{task_id}")
|
|
async def get_persona_task(task_id: str):
|
|
"""Get persona generation task status."""
|
|
try:
|
|
return await get_persona_task_status(task_id)
|
|
except Exception as e:
|
|
logger.error(f"Error in get_persona_task: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/step4/persona-latest")
|
|
async def persona_latest(current_user: dict = Depends(get_current_user)):
|
|
"""Get latest cached persona for current user."""
|
|
try:
|
|
return await get_latest_persona(current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in persona_latest: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step4/persona-save")
|
|
async def persona_save(request: dict, current_user: dict = Depends(get_current_user)):
|
|
"""Save edited persona back to cache."""
|
|
try:
|
|
return await save_persona_update(request, current_user)
|
|
except HTTPException as he:
|
|
raise he
|
|
except Exception as e:
|
|
logger.error(f"Error in persona_save: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step4/assess-persona-quality")
|
|
async def assess_persona_quality_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
|
"""Assess the quality of generated personas."""
|
|
try:
|
|
return await assess_persona_quality(request, current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in assess_persona_quality: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.post("/api/onboarding/step4/regenerate-persona")
|
|
async def regenerate_persona_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
|
"""Regenerate a specific persona with improvements."""
|
|
try:
|
|
return await regenerate_persona(request, current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in regenerate_persona: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@self.app.get("/api/onboarding/step4/persona-options")
|
|
async def get_persona_options(current_user: dict = Depends(get_current_user)):
|
|
"""Get persona generation options and configurations."""
|
|
try:
|
|
return await get_persona_generation_options(current_user)
|
|
except Exception as e:
|
|
logger.error(f"Error in get_persona_options: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
def get_onboarding_status(self) -> Dict[str, Any]:
|
|
"""Get the status of onboarding endpoints."""
|
|
return {
|
|
"onboarding_endpoints": [
|
|
"/api/onboarding/init",
|
|
"/api/onboarding/status",
|
|
"/api/onboarding/progress",
|
|
"/api/onboarding/step/{step_number}",
|
|
"/api/onboarding/step/{step_number}/complete",
|
|
"/api/onboarding/step/{step_number}/skip",
|
|
"/api/onboarding/step/{step_number}/validate",
|
|
"/api/onboarding/api-keys",
|
|
"/api/onboarding/api-keys/onboarding",
|
|
"/api/onboarding/start",
|
|
"/api/onboarding/complete",
|
|
"/api/onboarding/reset",
|
|
"/api/onboarding/resume",
|
|
"/api/onboarding/config",
|
|
"/api/onboarding/providers/{provider}/setup",
|
|
"/api/onboarding/providers",
|
|
"/api/onboarding/providers/{provider}/validate",
|
|
"/api/onboarding/validation/enhanced",
|
|
"/api/onboarding/summary",
|
|
"/api/onboarding/website-analysis",
|
|
"/api/onboarding/research-preferences",
|
|
"/api/onboarding/business-info",
|
|
"/api/onboarding/step4/generate-personas",
|
|
"/api/onboarding/step4/generate-personas-async",
|
|
"/api/onboarding/step4/persona-task/{task_id}",
|
|
"/api/onboarding/step4/persona-latest",
|
|
"/api/onboarding/step4/persona-save",
|
|
"/api/onboarding/step4/assess-persona-quality",
|
|
"/api/onboarding/step4/regenerate-persona",
|
|
"/api/onboarding/step4/persona-options"
|
|
],
|
|
"total_endpoints": 30,
|
|
"status": "active"
|
|
}
|