ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
610
backend/api/onboarding.py
Normal file
610
backend/api/onboarding.py
Normal file
@@ -0,0 +1,610 @@
|
||||
"""Onboarding API endpoints for ALwrity."""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from loguru import logger
|
||||
import time
|
||||
|
||||
# Import the existing progress tracking system
|
||||
from services.api_key_manager import (
|
||||
OnboardingProgress,
|
||||
get_onboarding_progress,
|
||||
StepStatus,
|
||||
StepData,
|
||||
APIKeyManager
|
||||
)
|
||||
from services.validation import check_all_api_keys
|
||||
|
||||
# Pydantic models for API requests/responses
|
||||
class StepDataModel(BaseModel):
|
||||
step_number: int
|
||||
title: str
|
||||
description: str
|
||||
status: str
|
||||
completed_at: Optional[str] = None
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
validation_errors: List[str] = []
|
||||
|
||||
class OnboardingProgressModel(BaseModel):
|
||||
steps: List[StepDataModel]
|
||||
current_step: int
|
||||
started_at: str
|
||||
last_updated: str
|
||||
is_completed: bool
|
||||
completed_at: Optional[str] = None
|
||||
|
||||
class StepCompletionRequest(BaseModel):
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
validation_errors: List[str] = []
|
||||
|
||||
class APIKeyRequest(BaseModel):
|
||||
provider: str = Field(..., description="API provider name (e.g., 'openai', 'gemini')")
|
||||
api_key: str = Field(..., description="API key value")
|
||||
description: Optional[str] = Field(None, description="Optional description")
|
||||
|
||||
class OnboardingStatusResponse(BaseModel):
|
||||
is_completed: bool
|
||||
current_step: int
|
||||
completion_percentage: float
|
||||
next_step: Optional[int]
|
||||
started_at: str
|
||||
completed_at: Optional[str] = None
|
||||
can_proceed_to_final: bool
|
||||
|
||||
class StepValidationResponse(BaseModel):
|
||||
can_proceed: bool
|
||||
validation_errors: List[str]
|
||||
step_status: str
|
||||
|
||||
# Dependency to get progress instance
|
||||
def get_progress() -> OnboardingProgress:
|
||||
"""Get the current onboarding progress instance."""
|
||||
return get_onboarding_progress()
|
||||
|
||||
# Dependency to get API key manager
|
||||
def get_api_key_manager() -> APIKeyManager:
|
||||
"""Get the API key manager instance."""
|
||||
return APIKeyManager()
|
||||
|
||||
# Health check endpoint
|
||||
def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
# Onboarding status endpoints
|
||||
async def get_onboarding_status():
|
||||
"""Get the current onboarding status."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
|
||||
# Safety check: if all steps are completed, ensure is_completed is True
|
||||
all_steps_completed = all(s.status in [StepStatus.COMPLETED, StepStatus.SKIPPED] for s in progress.steps)
|
||||
if all_steps_completed and not progress.is_completed:
|
||||
logger.info(f"[get_onboarding_status] All steps completed but is_completed was False, fixing...")
|
||||
progress.is_completed = True
|
||||
progress.completed_at = datetime.now().isoformat()
|
||||
progress.current_step = len(progress.steps) # Ensure current_step is valid
|
||||
progress.save_progress()
|
||||
|
||||
logger.info(f"[get_onboarding_status] Current step: {progress.current_step}")
|
||||
logger.info(f"[get_onboarding_status] Is completed: {progress.is_completed}")
|
||||
logger.info(f"[get_onboarding_status] Steps status: {[f'{s.step_number}:{s.status.value}' for s in progress.steps]}")
|
||||
|
||||
return OnboardingStatusResponse(
|
||||
is_completed=progress.is_completed,
|
||||
current_step=progress.current_step,
|
||||
completion_percentage=progress.get_completion_percentage(),
|
||||
next_step=progress.get_next_incomplete_step(),
|
||||
started_at=progress.started_at,
|
||||
completed_at=progress.completed_at,
|
||||
can_proceed_to_final=progress.can_complete_onboarding()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding status: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_onboarding_progress_full():
|
||||
"""Get the full onboarding progress data."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
# Convert StepData objects to Pydantic models
|
||||
step_models = []
|
||||
for step in progress.steps:
|
||||
step_models.append(StepDataModel(
|
||||
step_number=step.step_number,
|
||||
title=step.title,
|
||||
description=step.description,
|
||||
status=step.status.value,
|
||||
completed_at=step.completed_at,
|
||||
data=step.data,
|
||||
validation_errors=step.validation_errors or []
|
||||
))
|
||||
|
||||
return OnboardingProgressModel(
|
||||
steps=step_models,
|
||||
current_step=progress.current_step,
|
||||
started_at=progress.started_at,
|
||||
last_updated=progress.last_updated,
|
||||
is_completed=progress.is_completed,
|
||||
completed_at=progress.completed_at
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding progress: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_step_data(step_number: int):
|
||||
"""Get data for a specific step."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
step = progress.get_step_data(step_number)
|
||||
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
|
||||
|
||||
return StepDataModel(
|
||||
step_number=step.step_number,
|
||||
title=step.title,
|
||||
description=step.description,
|
||||
status=step.status.value,
|
||||
completed_at=step.completed_at,
|
||||
data=step.data,
|
||||
validation_errors=step.validation_errors or []
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting step data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def complete_step(step_number: int, request: StepCompletionRequest):
|
||||
"""Mark a step as completed."""
|
||||
try:
|
||||
logger.info(f"[complete_step] Completing step {step_number}")
|
||||
progress = get_onboarding_progress()
|
||||
step = progress.get_step_data(step_number)
|
||||
|
||||
if not step:
|
||||
logger.error(f"[complete_step] Step {step_number} not found")
|
||||
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
|
||||
|
||||
# Mark step as completed
|
||||
progress.mark_step_completed(step_number, request.data)
|
||||
logger.info(f"[complete_step] Step {step_number} completed successfully")
|
||||
|
||||
return {
|
||||
"message": f"Step {step_number} completed successfully",
|
||||
"step_number": step_number,
|
||||
"data": request.data
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error completing step: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def skip_step(step_number: int):
|
||||
"""Skip a step (for optional steps)."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
step = progress.get_step_data(step_number)
|
||||
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
|
||||
|
||||
# Mark step as skipped
|
||||
progress.mark_step_skipped(step_number)
|
||||
|
||||
return {
|
||||
"message": f"Step {step_number} skipped successfully",
|
||||
"step_number": step_number
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error skipping step: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def validate_step_access(step_number: int):
|
||||
"""Validate if user can access a specific step."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
|
||||
if not progress.can_proceed_to_step(step_number):
|
||||
return StepValidationResponse(
|
||||
can_proceed=False,
|
||||
validation_errors=[f"Cannot proceed to step {step_number}. Complete previous steps first."],
|
||||
step_status="locked"
|
||||
)
|
||||
|
||||
return StepValidationResponse(
|
||||
can_proceed=True,
|
||||
validation_errors=[],
|
||||
step_status="available"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating step access: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
# Simple cache for API keys
|
||||
_api_keys_cache = None
|
||||
_cache_timestamp = 0
|
||||
CACHE_DURATION = 30 # Cache for 30 seconds
|
||||
|
||||
async def get_api_keys():
|
||||
"""Get all configured API keys (masked)."""
|
||||
global _api_keys_cache, _cache_timestamp
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# Return cached result if still valid
|
||||
if _api_keys_cache and (current_time - _cache_timestamp) < CACHE_DURATION:
|
||||
logger.debug("Returning cached API keys")
|
||||
return _api_keys_cache
|
||||
|
||||
try:
|
||||
api_manager = APIKeyManager()
|
||||
api_manager.load_api_keys() # Load keys from environment
|
||||
api_keys = api_manager.api_keys # Get the loaded keys
|
||||
|
||||
# Mask the API keys for security
|
||||
masked_keys = {}
|
||||
for provider, key in api_keys.items():
|
||||
if key:
|
||||
masked_keys[provider] = "*" * (len(key) - 4) + key[-4:] if len(key) > 4 else "*" * len(key)
|
||||
else:
|
||||
masked_keys[provider] = None
|
||||
|
||||
result = {
|
||||
"api_keys": masked_keys,
|
||||
"total_providers": len(api_keys),
|
||||
"configured_providers": [k for k, v in api_keys.items() if v]
|
||||
}
|
||||
|
||||
# Cache the result
|
||||
_api_keys_cache = result
|
||||
_cache_timestamp = current_time
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting API keys: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def save_api_key(request: APIKeyRequest):
|
||||
"""Save an API key for a provider."""
|
||||
try:
|
||||
api_manager = APIKeyManager()
|
||||
success = api_manager.save_api_key(request.provider, request.api_key)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"message": f"API key for {request.provider} saved successfully",
|
||||
"provider": request.provider,
|
||||
"status": "saved"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Failed to save API key for {request.provider}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving API key: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def validate_api_keys():
|
||||
"""Validate all configured API keys."""
|
||||
try:
|
||||
api_manager = APIKeyManager()
|
||||
validation_results = check_all_api_keys(api_manager)
|
||||
|
||||
return {
|
||||
"validation_results": validation_results.get('results', {}),
|
||||
"all_valid": validation_results.get('all_valid', False),
|
||||
"total_providers": len(validation_results.get('results', {}))
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating API keys: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def start_onboarding():
|
||||
"""Start a new onboarding session."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
progress.reset_progress()
|
||||
|
||||
return {
|
||||
"message": "Onboarding started successfully",
|
||||
"current_step": progress.current_step,
|
||||
"started_at": progress.started_at
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting onboarding: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def complete_onboarding():
|
||||
"""Complete the onboarding process."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
|
||||
# Check which required steps are missing
|
||||
required_steps = [1, 2, 3, 6] # Steps 1, 2, 3, and 6 are required
|
||||
missing_steps = []
|
||||
|
||||
for step_num in required_steps:
|
||||
step = progress.get_step_data(step_num)
|
||||
if step and step.status not in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
|
||||
missing_steps.append(step.title)
|
||||
|
||||
if missing_steps:
|
||||
missing_steps_str = ", ".join(missing_steps)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot complete onboarding. The following steps must be completed first: {missing_steps_str}"
|
||||
)
|
||||
|
||||
# Additional validation: Check if API keys are configured
|
||||
api_manager = get_api_key_manager()
|
||||
api_keys = api_manager.get_all_keys()
|
||||
if not api_keys:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot complete onboarding. At least one AI provider API key must be configured."
|
||||
)
|
||||
|
||||
progress.complete_onboarding()
|
||||
|
||||
return {
|
||||
"message": "Onboarding completed successfully",
|
||||
"completed_at": progress.completed_at,
|
||||
"completion_percentage": 100.0
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error completing onboarding: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def reset_onboarding():
|
||||
"""Reset the onboarding progress."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
progress.reset_progress()
|
||||
|
||||
return {
|
||||
"message": "Onboarding progress reset successfully",
|
||||
"current_step": progress.current_step,
|
||||
"started_at": progress.started_at
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting onboarding: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_resume_info():
|
||||
"""Get information for resuming onboarding."""
|
||||
try:
|
||||
progress = get_onboarding_progress()
|
||||
|
||||
if progress.is_completed:
|
||||
return {
|
||||
"can_resume": False,
|
||||
"message": "Onboarding is already completed",
|
||||
"completion_percentage": 100.0
|
||||
}
|
||||
|
||||
resume_step = progress.get_resume_step()
|
||||
|
||||
return {
|
||||
"can_resume": True,
|
||||
"resume_step": resume_step,
|
||||
"current_step": progress.current_step,
|
||||
"completion_percentage": progress.get_completion_percentage(),
|
||||
"started_at": progress.started_at,
|
||||
"last_updated": progress.last_updated
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting resume info: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
def get_onboarding_config():
|
||||
"""Get onboarding configuration and requirements."""
|
||||
return {
|
||||
"total_steps": 6,
|
||||
"steps": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "AI LLM Providers",
|
||||
"description": "Configure AI language model providers",
|
||||
"required": True,
|
||||
"providers": ["openai", "gemini", "anthropic"]
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Website Analysis",
|
||||
"description": "Set up website analysis and crawling",
|
||||
"required": True
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"title": "AI Research",
|
||||
"description": "Configure AI research capabilities",
|
||||
"required": True
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"title": "Personalization",
|
||||
"description": "Set up personalization features",
|
||||
"required": False
|
||||
},
|
||||
{
|
||||
"number": 5,
|
||||
"title": "Integrations",
|
||||
"description": "Configure ALwrity integrations",
|
||||
"required": False
|
||||
},
|
||||
{
|
||||
"number": 6,
|
||||
"title": "Complete Setup",
|
||||
"description": "Finalize and complete onboarding",
|
||||
"required": True
|
||||
}
|
||||
],
|
||||
"requirements": {
|
||||
"min_api_keys": 1,
|
||||
"required_providers": ["openai"],
|
||||
"optional_providers": ["gemini", "anthropic"]
|
||||
}
|
||||
}
|
||||
|
||||
# Add new endpoints for enhanced functionality
|
||||
|
||||
async def get_provider_setup_info(provider: str):
|
||||
"""Get setup information for a specific provider."""
|
||||
try:
|
||||
providers_info = get_all_providers_info()
|
||||
if provider in providers_info:
|
||||
return providers_info[provider]
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Provider {provider} not found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting provider setup info: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_all_providers_info():
|
||||
"""Get setup information for all providers."""
|
||||
return {
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"description": "GPT-4 and GPT-3.5 models for content generation",
|
||||
"setup_url": "https://platform.openai.com/api-keys",
|
||||
"required_fields": ["api_key"],
|
||||
"optional_fields": ["organization_id"]
|
||||
},
|
||||
"gemini": {
|
||||
"name": "Google Gemini",
|
||||
"description": "Google's advanced AI models for content creation",
|
||||
"setup_url": "https://makersuite.google.com/app/apikey",
|
||||
"required_fields": ["api_key"],
|
||||
"optional_fields": []
|
||||
},
|
||||
"anthropic": {
|
||||
"name": "Anthropic",
|
||||
"description": "Claude models for sophisticated content generation",
|
||||
"setup_url": "https://console.anthropic.com/",
|
||||
"required_fields": ["api_key"],
|
||||
"optional_fields": []
|
||||
}
|
||||
}
|
||||
|
||||
async def validate_provider_key(provider: str, request: APIKeyRequest):
|
||||
"""Validate a specific provider's API key."""
|
||||
try:
|
||||
result = await validate_api_key(provider, request.api_key)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating provider key: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_enhanced_validation_status():
|
||||
"""Get enhanced validation status for all configured services."""
|
||||
try:
|
||||
return await check_all_api_keys(get_api_key_manager())
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting enhanced validation status: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
# New endpoints for FinalStep data loading
|
||||
async def get_onboarding_summary():
|
||||
"""Get comprehensive onboarding summary for FinalStep."""
|
||||
try:
|
||||
from services.database import get_db
|
||||
from services.website_analysis_service import WebsiteAnalysisService
|
||||
from services.research_preferences_service import ResearchPreferencesService
|
||||
|
||||
# Get current session (assuming session ID 1 for now)
|
||||
session_id = 1
|
||||
|
||||
# Get API keys
|
||||
api_manager = get_api_key_manager()
|
||||
api_keys = api_manager.get_all_keys()
|
||||
|
||||
# Get website analysis data
|
||||
db = next(get_db())
|
||||
website_service = WebsiteAnalysisService(db)
|
||||
website_analysis = website_service.get_analysis_by_session(session_id)
|
||||
|
||||
# Get research preferences
|
||||
research_service = ResearchPreferencesService(db)
|
||||
research_preferences = research_service.get_research_preferences(session_id)
|
||||
|
||||
# Get personalization settings (from research preferences)
|
||||
personalization_settings = None
|
||||
if research_preferences:
|
||||
personalization_settings = {
|
||||
'writing_style': research_preferences.get('writing_style', {}).get('tone', 'Professional'),
|
||||
'tone': research_preferences.get('writing_style', {}).get('voice', 'Formal'),
|
||||
'brand_voice': research_preferences.get('writing_style', {}).get('complexity', 'Trustworthy and Expert')
|
||||
}
|
||||
|
||||
return {
|
||||
"api_keys": api_keys,
|
||||
"website_url": website_analysis.get('website_url') if website_analysis else None,
|
||||
"style_analysis": website_analysis.get('style_analysis') if website_analysis else None,
|
||||
"research_preferences": research_preferences,
|
||||
"personalization_settings": personalization_settings,
|
||||
"integrations": {}, # TODO: Implement integrations data
|
||||
"capabilities": {
|
||||
"ai_content": len(api_keys) > 0,
|
||||
"style_analysis": website_analysis is not None,
|
||||
"research_tools": research_preferences is not None,
|
||||
"personalization": personalization_settings is not None,
|
||||
"integrations": False # TODO: Implement
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting onboarding summary: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_website_analysis_data():
|
||||
"""Get website analysis data for FinalStep."""
|
||||
try:
|
||||
from services.database import get_db
|
||||
from services.website_analysis_service import WebsiteAnalysisService
|
||||
|
||||
session_id = 1
|
||||
db = next(get_db())
|
||||
website_service = WebsiteAnalysisService(db)
|
||||
analysis = website_service.get_analysis_by_session(session_id)
|
||||
|
||||
if analysis:
|
||||
return {
|
||||
"website_url": analysis.get('website_url'),
|
||||
"style_analysis": analysis.get('style_analysis'),
|
||||
"style_patterns": analysis.get('style_patterns'),
|
||||
"style_guidelines": analysis.get('style_guidelines'),
|
||||
"status": analysis.get('status'),
|
||||
"completed_at": analysis.get('created_at')
|
||||
}
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting website analysis data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_research_preferences_data():
|
||||
"""Get research preferences data for FinalStep."""
|
||||
try:
|
||||
from services.database import get_db
|
||||
from services.research_preferences_service import ResearchPreferencesService
|
||||
|
||||
session_id = 1
|
||||
db = next(get_db())
|
||||
research_service = ResearchPreferencesService(db)
|
||||
preferences = research_service.get_research_preferences(session_id)
|
||||
|
||||
return preferences
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
Reference in New Issue
Block a user