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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, Body
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -64,6 +64,15 @@ async def activate_strategy_with_monitoring(
|
||||
if not monitoring_success:
|
||||
logger.warning(f"Failed to save monitoring data for strategy {strategy_id}")
|
||||
|
||||
# Trigger scheduler interval adjustment (scheduler will check more frequently now)
|
||||
try:
|
||||
from services.scheduler import get_scheduler
|
||||
scheduler = get_scheduler()
|
||||
await scheduler.trigger_interval_adjustment()
|
||||
logger.info(f"Triggered scheduler interval adjustment after strategy {strategy_id} activation")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not trigger scheduler interval adjustment: {e}")
|
||||
|
||||
logger.info(f"Successfully activated strategy {strategy_id} with monitoring")
|
||||
return {
|
||||
"success": True,
|
||||
@@ -396,6 +405,150 @@ async def get_monitoring_tasks(
|
||||
logger.error(f"Error retrieving monitoring tasks: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/user/{user_id}/monitoring-tasks")
|
||||
async def get_user_monitoring_tasks(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
status: Optional[str] = Query(None, description="Filter by task status"),
|
||||
limit: int = Query(50, description="Maximum number of tasks to return"),
|
||||
offset: int = Query(0, description="Number of tasks to skip")
|
||||
):
|
||||
"""
|
||||
Get all monitoring tasks for a specific user with their execution status.
|
||||
|
||||
Uses the scheduler's task loader to get tasks filtered by user_id for proper user isolation.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Getting monitoring tasks for user {user_id}")
|
||||
|
||||
# Use scheduler task loader for user-specific tasks
|
||||
from services.scheduler.utils.task_loader import load_due_monitoring_tasks
|
||||
|
||||
# Load all tasks for user (not just due tasks - we want all user tasks)
|
||||
# Join with strategy to filter by user
|
||||
tasks_query = db.query(MonitoringTask).join(
|
||||
EnhancedContentStrategy,
|
||||
MonitoringTask.strategy_id == EnhancedContentStrategy.id
|
||||
).filter(
|
||||
EnhancedContentStrategy.user_id == user_id
|
||||
)
|
||||
|
||||
# Apply status filter if provided
|
||||
if status:
|
||||
tasks_query = tasks_query.filter(MonitoringTask.status == status)
|
||||
|
||||
# Get tasks with pagination
|
||||
tasks = tasks_query.order_by(desc(MonitoringTask.created_at)).offset(offset).limit(limit).all()
|
||||
|
||||
tasks_data = []
|
||||
for task in tasks:
|
||||
# Get latest execution log
|
||||
latest_log = db.query(TaskExecutionLog).filter(
|
||||
TaskExecutionLog.task_id == task.id
|
||||
).order_by(desc(TaskExecutionLog.execution_date)).first()
|
||||
|
||||
# Get strategy info
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == task.strategy_id
|
||||
).first()
|
||||
|
||||
task_data = {
|
||||
"id": task.id,
|
||||
"strategy_id": task.strategy_id,
|
||||
"strategy_name": strategy.name if strategy else None,
|
||||
"title": task.task_title,
|
||||
"description": task.task_description,
|
||||
"assignee": task.assignee,
|
||||
"frequency": task.frequency,
|
||||
"metric": task.metric,
|
||||
"measurementMethod": task.measurement_method,
|
||||
"successCriteria": task.success_criteria,
|
||||
"alertThreshold": task.alert_threshold,
|
||||
"status": task.status,
|
||||
"lastExecuted": latest_log.execution_date.isoformat() if latest_log else None,
|
||||
"nextExecution": task.next_execution.isoformat() if task.next_execution else None,
|
||||
"executionCount": db.query(TaskExecutionLog).filter(
|
||||
TaskExecutionLog.task_id == task.id
|
||||
).count(),
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None
|
||||
}
|
||||
tasks_data.append(task_data)
|
||||
|
||||
# Get total count for pagination
|
||||
total_count = db.query(MonitoringTask).join(
|
||||
EnhancedContentStrategy,
|
||||
MonitoringTask.strategy_id == EnhancedContentStrategy.id
|
||||
).filter(
|
||||
EnhancedContentStrategy.user_id == user_id
|
||||
)
|
||||
if status:
|
||||
total_count = total_count.filter(MonitoringTask.status == status)
|
||||
total_count = total_count.count()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": tasks_data,
|
||||
"pagination": {
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + len(tasks_data)) < total_count
|
||||
},
|
||||
"message": f"Retrieved {len(tasks_data)} monitoring tasks for user {user_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving user monitoring tasks: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve monitoring tasks: {str(e)}")
|
||||
|
||||
@router.get("/user/{user_id}/execution-logs")
|
||||
async def get_user_execution_logs(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
status: Optional[str] = Query(None, description="Filter by execution status"),
|
||||
limit: int = Query(50, description="Maximum number of logs to return"),
|
||||
offset: int = Query(0, description="Number of logs to skip")
|
||||
):
|
||||
"""
|
||||
Get execution logs for a specific user.
|
||||
|
||||
Provides user isolation by filtering execution logs by user_id.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Getting execution logs for user {user_id}")
|
||||
|
||||
monitoring_service = MonitoringDataService(db)
|
||||
logs_data = monitoring_service.get_user_execution_logs(
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
status_filter=status
|
||||
)
|
||||
|
||||
# Get total count for pagination
|
||||
count_query = db.query(TaskExecutionLog).filter(
|
||||
TaskExecutionLog.user_id == user_id
|
||||
)
|
||||
if status:
|
||||
count_query = count_query.filter(TaskExecutionLog.status == status)
|
||||
total_count = count_query.count()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": logs_data,
|
||||
"pagination": {
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + len(logs_data)) < total_count
|
||||
},
|
||||
"message": f"Retrieved {len(logs_data)} execution logs for user {user_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving execution logs for user {user_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve execution logs: {str(e)}")
|
||||
|
||||
@router.get("/{strategy_id}/data-freshness")
|
||||
async def get_data_freshness(
|
||||
strategy_id: int,
|
||||
|
||||
@@ -3,13 +3,18 @@ from __future__ import annotations
|
||||
import base64
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from services.llm_providers.main_image_generation import generate_image
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from utils.logger_utils import get_service_logger
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.database import get_db
|
||||
from services.subscription import UsageTrackingService, PricingService
|
||||
from models.subscription_models import APIProvider, UsageSummary
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/images", tags=["images"])
|
||||
@@ -39,9 +44,23 @@ class ImageGenerateResponse(BaseModel):
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ImageGenerateResponse)
|
||||
def generate(req: ImageGenerateRequest) -> ImageGenerateResponse:
|
||||
def generate(
|
||||
req: ImageGenerateRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> ImageGenerateResponse:
|
||||
"""Generate image with subscription checking."""
|
||||
try:
|
||||
# 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")
|
||||
|
||||
# Validation is now handled inside generate_image function
|
||||
last_error: Optional[Exception] = None
|
||||
result = None
|
||||
for attempt in range(2): # simple single retry
|
||||
try:
|
||||
result = generate_image(
|
||||
@@ -56,8 +75,79 @@ def generate(req: ImageGenerateRequest) -> ImageGenerateResponse:
|
||||
"steps": req.steps,
|
||||
"seed": req.seed,
|
||||
},
|
||||
user_id=user_id, # Pass user_id for validation inside generate_image
|
||||
)
|
||||
image_b64 = base64.b64encode(result.image_bytes).decode("utf-8")
|
||||
|
||||
# TRACK USAGE after successful image generation
|
||||
if result:
|
||||
logger.info(f"[images.generate] ✅ Image generation successful, tracking usage for user {user_id}")
|
||||
try:
|
||||
db_track = next(get_db())
|
||||
try:
|
||||
# Get or create usage summary
|
||||
pricing = PricingService(db_track)
|
||||
current_period = pricing.get_current_billing_period(user_id) or datetime.now().strftime("%Y-%m")
|
||||
|
||||
logger.debug(f"[images.generate] Looking for usage summary: user_id={user_id}, period={current_period}")
|
||||
|
||||
summary = db_track.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period == current_period
|
||||
).first()
|
||||
|
||||
if not summary:
|
||||
logger.info(f"[images.generate] Creating new usage summary for user {user_id}, period {current_period}")
|
||||
summary = UsageSummary(
|
||||
user_id=user_id,
|
||||
billing_period=current_period
|
||||
)
|
||||
db_track.add(summary)
|
||||
db_track.flush() # Ensure summary is persisted before updating
|
||||
|
||||
# Get "before" state for unified log
|
||||
current_calls_before = getattr(summary, "stability_calls", 0) or 0
|
||||
|
||||
# Update provider-specific counters (stability for image generation)
|
||||
# Note: All image generation goes through STABILITY provider enum regardless of actual provider
|
||||
new_calls = current_calls_before + 1
|
||||
setattr(summary, "stability_calls", new_calls)
|
||||
logger.debug(f"[images.generate] Updated stability_calls: {current_calls_before} -> {new_calls}")
|
||||
|
||||
# Update totals
|
||||
old_total_calls = summary.total_calls or 0
|
||||
summary.total_calls = old_total_calls + 1
|
||||
logger.debug(f"[images.generate] Updated totals: calls {old_total_calls} -> {summary.total_calls}")
|
||||
|
||||
# Get plan details for unified log
|
||||
limits = pricing.get_user_limits(user_id)
|
||||
plan_name = limits.get('plan_name', 'unknown') if limits else 'unknown'
|
||||
tier = limits.get('tier', 'unknown') if limits else 'unknown'
|
||||
call_limit = limits['limits'].get("stability_calls", 0) if limits else 0
|
||||
|
||||
db_track.commit()
|
||||
logger.info(f"[images.generate] ✅ Successfully tracked usage: user {user_id} -> stability -> {new_calls} calls")
|
||||
|
||||
# UNIFIED SUBSCRIPTION LOG - Shows before/after state in one message
|
||||
print(f"""
|
||||
[SUBSCRIPTION] Image Generation
|
||||
├─ User: {user_id}
|
||||
├─ Plan: {plan_name} ({tier})
|
||||
├─ Provider: stability
|
||||
├─ Actual Provider: {result.provider}
|
||||
├─ Model: {result.model or 'default'}
|
||||
├─ Calls: {current_calls_before} → {new_calls} / {call_limit if call_limit > 0 else '∞'}
|
||||
└─ Status: ✅ Allowed & Tracked
|
||||
""")
|
||||
except Exception as track_error:
|
||||
logger.error(f"[images.generate] ❌ Error tracking usage (non-blocking): {track_error}", exc_info=True)
|
||||
db_track.rollback()
|
||||
finally:
|
||||
db_track.close()
|
||||
except Exception as usage_error:
|
||||
# Non-blocking: log error but don't fail the request
|
||||
logger.error(f"[images.generate] ❌ Failed to track usage: {usage_error}", exc_info=True)
|
||||
|
||||
return ImageGenerateResponse(
|
||||
image_base64=image_b64,
|
||||
width=result.width,
|
||||
@@ -106,7 +196,10 @@ class ImagePromptSuggestResponse(BaseModel):
|
||||
|
||||
|
||||
@router.post("/suggest-prompts", response_model=ImagePromptSuggestResponse)
|
||||
def suggest_prompts(req: ImagePromptSuggestRequest) -> ImagePromptSuggestResponse:
|
||||
def suggest_prompts(
|
||||
req: ImagePromptSuggestRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> ImagePromptSuggestResponse:
|
||||
try:
|
||||
provider = (req.provider or ("gemini" if (os.getenv("GPT_PROVIDER") or "").lower().startswith("gemini") else "huggingface")).lower()
|
||||
section = req.section or {}
|
||||
@@ -203,7 +296,15 @@ def suggest_prompts(req: ImagePromptSuggestRequest) -> ImagePromptSuggestRespons
|
||||
If including on-image text, return it in overlay_text (short: <= 8 words).
|
||||
"""
|
||||
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema)
|
||||
# Get user_id for llm_text_gen subscription check (required)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id_for_llm = str(current_user.get('id', ''))
|
||||
if not user_id_for_llm:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema, user_id=user_id_for_llm)
|
||||
data = raw if isinstance(raw, dict) else {}
|
||||
suggestions = data.get("suggestions") or []
|
||||
# basic fallback if provider returns string
|
||||
|
||||
@@ -94,6 +94,7 @@ async def get_subscription_plans(
|
||||
"description": plan.description,
|
||||
"features": plan.features or [],
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": plan.gemini_calls_limit,
|
||||
"openai_calls": plan.openai_calls_limit,
|
||||
"anthropic_calls": plan.anthropic_calls_limit,
|
||||
@@ -162,6 +163,7 @@ async def get_user_subscription(
|
||||
},
|
||||
"status": "free",
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(free_plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": free_plan.gemini_calls_limit,
|
||||
"openai_calls": free_plan.openai_calls_limit,
|
||||
"anthropic_calls": free_plan.anthropic_calls_limit,
|
||||
@@ -200,6 +202,7 @@ async def get_user_subscription(
|
||||
"is_free": False
|
||||
},
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(subscription.plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": subscription.plan.gemini_calls_limit,
|
||||
"openai_calls": subscription.plan.openai_calls_limit,
|
||||
"anthropic_calls": subscription.plan.anthropic_calls_limit,
|
||||
@@ -252,6 +255,7 @@ async def get_subscription_status(
|
||||
"tier": "free",
|
||||
"can_use_api": True,
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(free_plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": free_plan.gemini_calls_limit,
|
||||
"openai_calls": free_plan.openai_calls_limit,
|
||||
"anthropic_calls": free_plan.anthropic_calls_limit,
|
||||
@@ -309,6 +313,7 @@ async def get_subscription_status(
|
||||
"tier": subscription.plan.tier.value,
|
||||
"can_use_api": True,
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(subscription.plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": subscription.plan.gemini_calls_limit,
|
||||
"openai_calls": subscription.plan.openai_calls_limit,
|
||||
"anthropic_calls": subscription.plan.anthropic_calls_limit,
|
||||
@@ -331,9 +336,14 @@ async def get_subscription_status(
|
||||
async def subscribe_to_plan(
|
||||
user_id: str,
|
||||
subscription_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Create or update a user's subscription."""
|
||||
"""Create or update a user's subscription (renewal)."""
|
||||
|
||||
# Verify user can only subscribe/renew their own subscription
|
||||
if current_user.get('id') != user_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
try:
|
||||
plan_id = subscription_data.get('plan_id')
|
||||
@@ -388,12 +398,75 @@ async def subscribe_to_plan(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Get current usage BEFORE reset for logging
|
||||
current_period = datetime.utcnow().strftime("%Y-%m")
|
||||
usage_before = db.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period == current_period
|
||||
).first()
|
||||
|
||||
# Log renewal request details
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"[SUBSCRIPTION RENEWAL] 🔄 Processing renewal request")
|
||||
logger.info(f" ├─ User: {user_id}")
|
||||
logger.info(f" ├─ Plan: {plan.name} (ID: {plan_id}, Tier: {plan.tier.value})")
|
||||
logger.info(f" ├─ Billing Cycle: {billing_cycle}")
|
||||
logger.info(f" ├─ Period Start: {now.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info(f" └─ Period End: {subscription.current_period_end.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
if usage_before:
|
||||
logger.info(f" 📊 Current Usage BEFORE Reset (Period: {current_period}):")
|
||||
logger.info(f" ├─ Gemini: {usage_before.gemini_tokens or 0} tokens / {usage_before.gemini_calls or 0} calls")
|
||||
logger.info(f" ├─ Mistral/HF: {usage_before.mistral_tokens or 0} tokens / {usage_before.mistral_calls or 0} calls")
|
||||
logger.info(f" ├─ OpenAI: {usage_before.openai_tokens or 0} tokens / {usage_before.openai_calls or 0} calls")
|
||||
logger.info(f" ├─ Stability (Images): {usage_before.stability_calls or 0} calls")
|
||||
logger.info(f" ├─ Total Tokens: {usage_before.total_tokens or 0}")
|
||||
logger.info(f" ├─ Total Calls: {usage_before.total_calls or 0}")
|
||||
logger.info(f" └─ Usage Status: {usage_before.usage_status.value}")
|
||||
else:
|
||||
logger.info(f" 📊 No usage summary found for period {current_period} (will be created on reset)")
|
||||
|
||||
# Clear subscription limits cache to force refresh on next check
|
||||
try:
|
||||
from services.subscription import PricingService
|
||||
# Clear cache for this specific user (class-level cache shared across all instances)
|
||||
cleared_count = PricingService.clear_user_cache(user_id)
|
||||
logger.info(f" 🗑️ Cleared {cleared_count} subscription cache entries for user {user_id}")
|
||||
except Exception as cache_err:
|
||||
logger.error(f" ❌ Failed to clear cache after subscribe: {cache_err}")
|
||||
|
||||
# Reset usage status for current billing period so new plan takes effect immediately
|
||||
reset_result = None
|
||||
try:
|
||||
usage_service = UsageTrackingService(db)
|
||||
await usage_service.reset_current_billing_period(user_id)
|
||||
reset_result = await usage_service.reset_current_billing_period(user_id)
|
||||
|
||||
# Re-query usage summary from DB after reset to get fresh data
|
||||
usage_after = db.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period == current_period
|
||||
).first()
|
||||
|
||||
if reset_result.get('reset'):
|
||||
logger.info(f" ✅ Usage counters RESET successfully")
|
||||
if usage_after:
|
||||
logger.info(f" 📊 New Usage AFTER Reset:")
|
||||
logger.info(f" ├─ Gemini: {usage_after.gemini_tokens or 0} tokens / {usage_after.gemini_calls or 0} calls")
|
||||
logger.info(f" ├─ Mistral/HF: {usage_after.mistral_tokens or 0} tokens / {usage_after.mistral_calls or 0} calls")
|
||||
logger.info(f" ├─ OpenAI: {usage_after.openai_tokens or 0} tokens / {usage_after.openai_calls or 0} calls")
|
||||
logger.info(f" ├─ Stability (Images): {usage_after.stability_calls or 0} calls")
|
||||
logger.info(f" ├─ Total Tokens: {usage_after.total_tokens or 0}")
|
||||
logger.info(f" ├─ Total Calls: {usage_after.total_calls or 0}")
|
||||
logger.info(f" └─ Usage Status: {usage_after.usage_status.value}")
|
||||
else:
|
||||
logger.warning(f" ⚠️ Usage summary not found after reset - may need to be created on next API call")
|
||||
else:
|
||||
logger.warning(f" ⚠️ Reset returned: {reset_result.get('reason', 'unknown')}")
|
||||
except Exception as reset_err:
|
||||
logger.error(f"Failed to reset usage after subscribe: {reset_err}")
|
||||
logger.error(f" ❌ Failed to reset usage after subscribe: {reset_err}", exc_info=True)
|
||||
|
||||
logger.info(f" ✅ Renewal completed: User {user_id} → {plan.name} ({billing_cycle})")
|
||||
logger.info("=" * 80)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -404,7 +477,20 @@ async def subscribe_to_plan(
|
||||
"billing_cycle": billing_cycle,
|
||||
"current_period_start": subscription.current_period_start.isoformat(),
|
||||
"current_period_end": subscription.current_period_end.isoformat(),
|
||||
"status": subscription.status.value
|
||||
"status": subscription.status.value,
|
||||
"limits": {
|
||||
"ai_text_generation_calls": getattr(plan, 'ai_text_generation_calls_limit', None) or 0,
|
||||
"gemini_calls": plan.gemini_calls_limit,
|
||||
"openai_calls": plan.openai_calls_limit,
|
||||
"anthropic_calls": plan.anthropic_calls_limit,
|
||||
"mistral_calls": plan.mistral_calls_limit,
|
||||
"tavily_calls": plan.tavily_calls_limit,
|
||||
"serper_calls": plan.serper_calls_limit,
|
||||
"metaphor_calls": plan.metaphor_calls_limit,
|
||||
"firecrawl_calls": plan.firecrawl_calls_limit,
|
||||
"stability_calls": plan.stability_calls_limit,
|
||||
"monthly_cost": plan.monthly_cost_limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -477,6 +477,39 @@ async def test_publish_to_wix(request: WixPublishRequest) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/refresh-token")
|
||||
async def refresh_wix_token(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Refresh Wix access token using refresh token
|
||||
|
||||
Args:
|
||||
request: Dict containing refresh_token
|
||||
|
||||
Returns:
|
||||
New token information with access_token, refresh_token, expires_in
|
||||
"""
|
||||
try:
|
||||
refresh_token = request.get("refresh_token")
|
||||
if not refresh_token:
|
||||
raise HTTPException(status_code=400, detail="Missing refresh_token")
|
||||
|
||||
# Refresh the token
|
||||
new_tokens = wix_service.refresh_access_token(refresh_token)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"access_token": new_tokens.get("access_token"),
|
||||
"refresh_token": new_tokens.get("refresh_token"),
|
||||
"expires_in": new_tokens.get("expires_in"),
|
||||
"token_type": new_tokens.get("token_type", "Bearer")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refresh Wix token: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to refresh token: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/test/publish/real")
|
||||
async def test_publish_real(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user