Subscription dashboard improvements, AI text generation limit, and other fixes.
This commit is contained in:
@@ -5,10 +5,11 @@ Main router for blog writing operations including research, outline generation,
|
||||
content creation, SEO analysis, and publishing.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from loguru import logger
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
from models.blog_models import (
|
||||
BlogResearchRequest,
|
||||
@@ -64,10 +65,21 @@ class SEOApplyRecommendationsRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/seo/apply-recommendations")
|
||||
async def apply_seo_recommendations(request: SEOApplyRecommendationsRequest) -> Dict[str, Any]:
|
||||
async def apply_seo_recommendations(
|
||||
request: SEOApplyRecommendationsRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply actionable SEO recommendations and return updated content."""
|
||||
try:
|
||||
result = await recommendation_applier.apply_recommendations(request.dict())
|
||||
# Extract Clerk user ID (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id = str(current_user.get('id', ''))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
result = await recommendation_applier.apply_recommendations(request.dict(), user_id=user_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Failed to apply recommendations"))
|
||||
return result
|
||||
@@ -87,13 +99,24 @@ async def health() -> Dict[str, Any]:
|
||||
|
||||
# Research Endpoints
|
||||
@router.post("/research/start")
|
||||
async def start_research(request: BlogResearchRequest) -> Dict[str, Any]:
|
||||
async def start_research(
|
||||
request: BlogResearchRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Start a research operation and return a task ID for polling."""
|
||||
try:
|
||||
# TODO: Get user_id from authentication context
|
||||
user_id = "anonymous" # This should come from auth middleware
|
||||
# Extract Clerk user ID (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id = str(current_user.get('id', ''))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
task_id = await task_manager.start_research_task(request, user_id)
|
||||
return {"task_id": task_id, "status": "started"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start research: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -107,6 +130,50 @@ async def get_research_status(task_id: str) -> Dict[str, Any]:
|
||||
if status is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
# If task failed with subscription error, return HTTP error so frontend interceptor can catch it
|
||||
if status.get('status') == 'failed' and status.get('error_status') in [429, 402]:
|
||||
error_data = status.get('error_data', {}) or {}
|
||||
error_status = status.get('error_status', 429)
|
||||
|
||||
if not isinstance(error_data, dict):
|
||||
logger.warning(f"Research task {task_id} error_data not dict: {error_data}")
|
||||
error_data = {'error': str(error_data)}
|
||||
|
||||
# Determine provider and usage info
|
||||
stored_error_message = status.get('error', error_data.get('error'))
|
||||
provider = error_data.get('provider', 'unknown')
|
||||
usage_info = error_data.get('usage_info')
|
||||
|
||||
if not usage_info:
|
||||
usage_info = {
|
||||
'provider': provider,
|
||||
'message': stored_error_message,
|
||||
'error_type': error_data.get('error_type', 'unknown')
|
||||
}
|
||||
# Include any known fields from error_data
|
||||
for key in ['current_tokens', 'requested_tokens', 'limit', 'current_calls']:
|
||||
if key in error_data:
|
||||
usage_info[key] = error_data[key]
|
||||
|
||||
# Build error message for detail
|
||||
error_msg = error_data.get('message', stored_error_message or 'Subscription limit exceeded')
|
||||
|
||||
# Log the subscription error with all context
|
||||
logger.warning(f"Research task {task_id} failed with subscription error {error_status}: {error_msg}")
|
||||
logger.warning(f" Provider: {provider}, Usage Info: {usage_info}")
|
||||
|
||||
# Use JSONResponse to ensure detail is returned as-is, not wrapped in an array
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(
|
||||
status_code=error_status,
|
||||
content={
|
||||
'error': error_data.get('error', stored_error_message or 'Subscription limit exceeded'),
|
||||
'message': error_msg,
|
||||
'provider': provider,
|
||||
'usage_info': usage_info
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Research status request for {task_id}: {status['status']} with {len(status.get('progress_messages', []))} progress messages")
|
||||
return status
|
||||
except HTTPException:
|
||||
@@ -310,20 +377,46 @@ async def hallucination_check(request: HallucinationCheckRequest) -> Hallucinati
|
||||
|
||||
# SEO Endpoints
|
||||
@router.post("/seo/analyze", response_model=BlogSEOAnalyzeResponse)
|
||||
async def seo_analyze(request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
|
||||
async def seo_analyze(
|
||||
request: BlogSEOAnalyzeRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> BlogSEOAnalyzeResponse:
|
||||
"""Analyze content for SEO optimization opportunities."""
|
||||
try:
|
||||
return await service.seo_analyze(request)
|
||||
# Extract Clerk user ID (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id = str(current_user.get('id', ''))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
return await service.seo_analyze(request, user_id=user_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to perform SEO analysis: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/seo/metadata", response_model=BlogSEOMetadataResponse)
|
||||
async def seo_metadata(request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
|
||||
async def seo_metadata(
|
||||
request: BlogSEOMetadataRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> BlogSEOMetadataResponse:
|
||||
"""Generate SEO metadata for the blog post."""
|
||||
try:
|
||||
return await service.seo_metadata(request)
|
||||
# Extract Clerk user ID (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id = str(current_user.get('id', ''))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
return await service.seo_metadata(request, user_id=user_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate SEO metadata: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -10,6 +10,7 @@ import asyncio
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from models.blog_models import (
|
||||
@@ -85,6 +86,10 @@ class TaskManager:
|
||||
response["result"] = task["result"]
|
||||
elif task["status"] == "failed":
|
||||
response["error"] = task["error"]
|
||||
if "error_status" in task:
|
||||
response["error_status"] = task["error_status"]
|
||||
if "error_data" in task:
|
||||
response["error_data"] = task["error_data"]
|
||||
|
||||
return response
|
||||
|
||||
@@ -109,14 +114,17 @@ class TaskManager:
|
||||
|
||||
logger.info(f"Progress update for task {task_id}: {message}")
|
||||
|
||||
async def start_research_task(self, request: BlogResearchRequest, user_id: str = "anonymous") -> str:
|
||||
async def start_research_task(self, request: BlogResearchRequest, user_id: str) -> str:
|
||||
"""Start a research operation and return a task ID."""
|
||||
if self.use_database:
|
||||
return await self.db_manager.start_research_task(request, user_id)
|
||||
else:
|
||||
task_id = self.create_task("research")
|
||||
# Store user_id in task for subscription checks
|
||||
if task_id in self.task_storage:
|
||||
self.task_storage[task_id]["user_id"] = user_id
|
||||
# Start the research operation in the background
|
||||
asyncio.create_task(self._run_research_task(task_id, request))
|
||||
asyncio.create_task(self._run_research_task(task_id, request, user_id))
|
||||
return task_id
|
||||
|
||||
def start_outline_task(self, request: BlogOutlineRequest) -> str:
|
||||
@@ -144,7 +152,7 @@ class TaskManager:
|
||||
asyncio.create_task(self._run_medium_generation_task(task_id, request))
|
||||
return task_id
|
||||
|
||||
async def _run_research_task(self, task_id: str, request: BlogResearchRequest):
|
||||
async def _run_research_task(self, task_id: str, request: BlogResearchRequest, user_id: str):
|
||||
"""Background task to run research and update status with progress messages."""
|
||||
try:
|
||||
# Update status to running
|
||||
@@ -157,8 +165,8 @@ class TaskManager:
|
||||
# Check cache first
|
||||
await self.update_progress(task_id, "📋 Checking cache for existing research...")
|
||||
|
||||
# Run the actual research with progress updates
|
||||
result = await self.service.research_with_progress(request, task_id)
|
||||
# Run the actual research with progress updates (pass user_id for subscription checks)
|
||||
result = await self.service.research_with_progress(request, task_id, user_id)
|
||||
|
||||
# Check if research failed gracefully
|
||||
if not result.success:
|
||||
@@ -171,6 +179,16 @@ class TaskManager:
|
||||
self.task_storage[task_id]["status"] = "completed"
|
||||
self.task_storage[task_id]["result"] = result.dict()
|
||||
|
||||
except HTTPException as http_error:
|
||||
# Handle HTTPException (e.g., 429 subscription limit) - preserve error details for frontend
|
||||
error_detail = http_error.detail
|
||||
error_message = error_detail.get('message', str(error_detail)) if isinstance(error_detail, dict) else str(error_detail)
|
||||
await self.update_progress(task_id, f"❌ {error_message}")
|
||||
self.task_storage[task_id]["status"] = "failed"
|
||||
self.task_storage[task_id]["error"] = error_message
|
||||
# Store HTTP error details for frontend modal
|
||||
self.task_storage[task_id]["error_status"] = http_error.status_code
|
||||
self.task_storage[task_id]["error_data"] = error_detail if isinstance(error_detail, dict) else {"error": str(error_detail)}
|
||||
except Exception as e:
|
||||
await self.update_progress(task_id, f"❌ Research failed with error: {str(e)}")
|
||||
# Update status to failed
|
||||
|
||||
Reference in New Issue
Block a user