Commit_all_local_changes_after_PR_406_merge
This commit is contained in:
@@ -45,11 +45,17 @@ def _extract_error_message(exc: Exception) -> str:
|
||||
"""
|
||||
Extract user-friendly error message from exception.
|
||||
Handles HTTPException with nested error details from WaveSpeed API.
|
||||
Preserves subscription modal flags for frontend.
|
||||
"""
|
||||
if isinstance(exc, HTTPException):
|
||||
detail = exc.detail
|
||||
# If detail is a dict (from WaveSpeed client)
|
||||
if isinstance(detail, dict):
|
||||
# Check if this is a subscription/credit error
|
||||
if detail.get("error_type") == "insufficient_credits" or detail.get("show_subscription_modal"):
|
||||
# Return the error message with subscription modal flag
|
||||
return detail.get("message", "Insufficient credits. Please top up your account.")
|
||||
|
||||
# Try to extract message from nested response JSON
|
||||
response_str = detail.get("response", "")
|
||||
if response_str:
|
||||
@@ -86,6 +92,27 @@ def _extract_error_message(exc: Exception) -> str:
|
||||
return error_str
|
||||
|
||||
|
||||
def _extract_error_metadata(exc: Exception) -> Dict[str, Any]:
|
||||
"""Extract structured error metadata for task polling clients."""
|
||||
if isinstance(exc, HTTPException):
|
||||
detail = exc.detail
|
||||
if isinstance(detail, dict):
|
||||
return {
|
||||
"error_status": exc.status_code,
|
||||
"error_data": detail,
|
||||
}
|
||||
if isinstance(detail, str):
|
||||
return {
|
||||
"error_status": exc.status_code,
|
||||
"error_data": {
|
||||
"error": detail,
|
||||
"message": detail,
|
||||
},
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _execute_podcast_video_task(
|
||||
task_id: str,
|
||||
request: PodcastVideoGenerationRequest,
|
||||
@@ -229,9 +256,15 @@ def _execute_podcast_video_task(
|
||||
|
||||
# Extract user-friendly error message from exception
|
||||
error_msg = _extract_error_message(exc)
|
||||
error_meta = _extract_error_metadata(exc)
|
||||
|
||||
task_manager.update_task_status(
|
||||
task_id, "failed", error=error_msg, message=f"Video generation failed: {error_msg}"
|
||||
task_id,
|
||||
"failed",
|
||||
error=error_msg,
|
||||
message=f"Video generation failed: {error_msg}",
|
||||
error_status=error_meta.get("error_status"),
|
||||
error_data=error_meta.get("error_data"),
|
||||
)
|
||||
|
||||
|
||||
@@ -257,7 +290,7 @@ async def generate_podcast_video(
|
||||
try:
|
||||
if hasattr(request, 'headers') and hasattr(request.headers, 'get'):
|
||||
auth_header = request.headers.get("Authorization")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
|
||||
@@ -76,6 +76,10 @@ class TaskManager:
|
||||
|
||||
if task["status"] == "failed" and task.get("error"):
|
||||
response["error"] = task["error"]
|
||||
if task.get("error_status") is not None:
|
||||
response["error_status"] = task["error_status"]
|
||||
if task.get("error_data") is not None:
|
||||
response["error_data"] = task["error_data"]
|
||||
|
||||
return response
|
||||
|
||||
@@ -86,7 +90,9 @@ class TaskManager:
|
||||
progress: Optional[float] = None,
|
||||
message: Optional[str] = None,
|
||||
result: Optional[Dict[str, Any]] = None,
|
||||
error: Optional[str] = None
|
||||
error: Optional[str] = None,
|
||||
error_status: Optional[int] = None,
|
||||
error_data: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Update the status of a task."""
|
||||
if task_id not in self.task_storage:
|
||||
@@ -112,6 +118,10 @@ class TaskManager:
|
||||
if error is not None:
|
||||
task["error"] = error
|
||||
logger.error(f"[StoryWriter] Task {task_id} error: {error}")
|
||||
if error_status is not None:
|
||||
task["error_status"] = error_status
|
||||
if error_data is not None:
|
||||
task["error_data"] = error_data
|
||||
|
||||
async def execute_story_generation_task(
|
||||
self,
|
||||
|
||||
@@ -11,7 +11,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.database import get_db
|
||||
from services.today_workflow_service import get_or_create_daily_workflow_plan, update_task_status
|
||||
from services.today_workflow_service import get_or_create_daily_workflow_plan, update_task_status, _today_date_str
|
||||
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
|
||||
import asyncio
|
||||
from services.intelligence.txtai_service import TxtaiIntelligenceService
|
||||
@@ -81,26 +81,7 @@ async def _index_tasks_to_sif(user_id: str, date: str, tasks: list[dict], label:
|
||||
logger.debug(f"Background indexing failed for user {user_id}: {e}")
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_today_workflow(
|
||||
date: Optional[str] = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
user_id = str(current_user.get("id"))
|
||||
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date)
|
||||
|
||||
def _fetch_tasks():
|
||||
return (
|
||||
db.query(DailyWorkflowTask)
|
||||
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
||||
.order_by(DailyWorkflowTask.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
tasks = await run_in_threadpool(_fetch_tasks)
|
||||
|
||||
def _build_workflow_payload(user_id: str, plan: DailyWorkflowPlan, tasks: list[DailyWorkflowTask]) -> Dict[str, Any]:
|
||||
response_tasks = []
|
||||
for t in tasks:
|
||||
response_tasks.append(
|
||||
@@ -136,8 +117,156 @@ async def get_today_workflow(
|
||||
workflow_status = "completed"
|
||||
|
||||
total_estimated = int(sum(int(t.get("estimatedTime") or 0) for t in response_tasks))
|
||||
plan_json = plan.plan_json or {}
|
||||
|
||||
return {
|
||||
"workflow": {
|
||||
"id": f"daily-{user_id}-{plan.date}",
|
||||
"date": plan.date,
|
||||
"userId": user_id,
|
||||
"tasks": response_tasks,
|
||||
"currentTaskIndex": current_index,
|
||||
"completedTasks": completed,
|
||||
"totalTasks": total,
|
||||
"workflowStatus": workflow_status,
|
||||
"totalEstimatedTime": total_estimated,
|
||||
"actualTimeSpent": 0,
|
||||
},
|
||||
"plan": {
|
||||
"id": plan.id,
|
||||
"date": plan.date,
|
||||
"source": plan.source,
|
||||
"generation_mode": plan.generation_mode,
|
||||
"committee_agent_count": plan.committee_agent_count,
|
||||
"fallback_used": bool(plan.fallback_used),
|
||||
"quality_status": plan_json.get("quality_status", "contextual"),
|
||||
"contextuality_validation": plan_json.get("contextuality_validation"),
|
||||
"provenance_summary": {
|
||||
"generationMode": plan.generation_mode,
|
||||
"committeeAgentCount": plan.committee_agent_count,
|
||||
"fallbackUsed": bool(plan.fallback_used),
|
||||
"taskSourceBreakdown": {},
|
||||
},
|
||||
"created_at": plan.created_at.isoformat() if plan.created_at else None,
|
||||
"updated_at": plan.updated_at.isoformat() if plan.updated_at else None,
|
||||
},
|
||||
"schedule_status": {
|
||||
"date": plan.date,
|
||||
"generated": True,
|
||||
"scheduled_run_completed": plan.source == "scheduled",
|
||||
"source": plan.source,
|
||||
"created_at": plan.created_at.isoformat() if plan.created_at else None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_today_workflow(
|
||||
date: Optional[str] = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Get existing daily workflow for the specified date.
|
||||
Returns 404 if no workflow exists for the date.
|
||||
Workflow should only be created via explicit user action or scheduled job.
|
||||
"""
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
user_id = str(current_user.get("id"))
|
||||
date_str = date or _today_date_str()
|
||||
|
||||
def _get_existing():
|
||||
return (
|
||||
db.query(DailyWorkflowPlan)
|
||||
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
|
||||
.first()
|
||||
)
|
||||
|
||||
plan = await run_in_threadpool(_get_existing)
|
||||
|
||||
if not plan:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No workflow found for date {date_str}. Workflow should be generated via explicit user action or scheduled job."
|
||||
)
|
||||
|
||||
def _fetch_tasks():
|
||||
return (
|
||||
db.query(DailyWorkflowTask)
|
||||
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
||||
.order_by(DailyWorkflowTask.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
tasks = await run_in_threadpool(_fetch_tasks)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": _build_workflow_payload(user_id, plan, tasks),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_today_workflow_status(
|
||||
date: Optional[str] = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
user_id = str(current_user.get("id"))
|
||||
date_str = date or _today_date_str()
|
||||
|
||||
def _get_existing():
|
||||
return (
|
||||
db.query(DailyWorkflowPlan)
|
||||
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
|
||||
.first()
|
||||
)
|
||||
|
||||
plan = await run_in_threadpool(_get_existing)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"date": date_str,
|
||||
"generated": plan is not None,
|
||||
"scheduled_run_completed": bool(plan and plan.source == "scheduled"),
|
||||
"source": plan.source if plan else None,
|
||||
"created_at": plan.created_at.isoformat() if plan and plan.created_at else None,
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_workflow(
|
||||
date: Optional[str] = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
"""Explicitly generate a new daily workflow for the specified date.
|
||||
This should only be called when the user explicitly requests workflow generation
|
||||
or via a scheduled job at night.
|
||||
"""
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
user_id = str(current_user.get("id"))
|
||||
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date, creation_source="manual")
|
||||
|
||||
def _fetch_tasks():
|
||||
return (
|
||||
db.query(DailyWorkflowTask)
|
||||
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
|
||||
.order_by(DailyWorkflowTask.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
tasks = await run_in_threadpool(_fetch_tasks)
|
||||
|
||||
if created:
|
||||
response_tasks = _build_workflow_payload(user_id, plan, tasks)["workflow"]["tasks"]
|
||||
asyncio.create_task(_index_tasks_to_sif(user_id, plan.date, response_tasks, label="today"))
|
||||
from datetime import date as date_type, timedelta
|
||||
|
||||
@@ -200,29 +329,7 @@ async def get_today_workflow(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"workflow": {
|
||||
"id": f"daily-{user_id}-{plan.date}",
|
||||
"date": plan.date,
|
||||
"userId": user_id,
|
||||
"tasks": response_tasks,
|
||||
"currentTaskIndex": current_index,
|
||||
"completedTasks": completed,
|
||||
"totalTasks": total,
|
||||
"workflowStatus": workflow_status,
|
||||
"totalEstimatedTime": total_estimated,
|
||||
"actualTimeSpent": 0,
|
||||
},
|
||||
"plan": {
|
||||
"id": plan.id,
|
||||
"date": plan.date,
|
||||
"source": plan.source,
|
||||
"quality_status": (plan.plan_json or {}).get("quality_status", "contextual"),
|
||||
"contextuality_validation": (plan.plan_json or {}).get("contextuality_validation"),
|
||||
"created_at": plan.created_at.isoformat() if plan.created_at else None,
|
||||
"updated_at": plan.updated_at.isoformat() if plan.updated_at else None,
|
||||
},
|
||||
},
|
||||
"data": _build_workflow_payload(user_id, plan, tasks),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,26 @@ router = APIRouter()
|
||||
UPLOAD_DIR = Path("backend/data/video_studio/uploads")
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _extract_error_metadata(exc: Exception) -> Dict[str, Any]:
|
||||
"""Extract structured HTTP error metadata for polling clients."""
|
||||
if isinstance(exc, HTTPException):
|
||||
detail = exc.detail
|
||||
if isinstance(detail, dict):
|
||||
return {
|
||||
"error_status": exc.status_code,
|
||||
"error_data": detail,
|
||||
}
|
||||
if isinstance(detail, str):
|
||||
return {
|
||||
"error_status": exc.status_code,
|
||||
"error_data": {
|
||||
"error": detail,
|
||||
"message": detail,
|
||||
},
|
||||
}
|
||||
return {}
|
||||
|
||||
def _process_avatar_generation(task_id: str, image_path: Path, audio_path: Path, user_id: str, resolution: str, model: str):
|
||||
"""
|
||||
Background task to process avatar generation using shared InfiniteTalk service.
|
||||
@@ -94,7 +114,15 @@ def _process_avatar_generation(task_id: str, image_path: Path, audio_path: Path,
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[VideoStudio] Avatar generation failed for task {task_id}: {e}", exc_info=True)
|
||||
task_manager.update_task(task_id, "failed", error=str(e), user_id=user_id)
|
||||
error_meta = _extract_error_metadata(e)
|
||||
task_manager.update_task(
|
||||
task_id,
|
||||
"failed",
|
||||
error=str(e),
|
||||
user_id=user_id,
|
||||
error_status=error_meta.get("error_status"),
|
||||
error_data=error_meta.get("error_data"),
|
||||
)
|
||||
finally:
|
||||
# Cleanup temp upload files
|
||||
try:
|
||||
|
||||
@@ -39,7 +39,18 @@ class TaskManager:
|
||||
logger.error(f"[VideoStudio] Failed to create task: {e}")
|
||||
raise
|
||||
|
||||
def update_task(self, task_id: str, status: str, result: Optional[Dict] = None, error: Optional[str] = None, user_id: str = None, progress: float = None, message: str = None):
|
||||
def update_task(
|
||||
self,
|
||||
task_id: str,
|
||||
status: str,
|
||||
result: Optional[Dict] = None,
|
||||
error: Optional[str] = None,
|
||||
user_id: str = None,
|
||||
progress: float = None,
|
||||
message: str = None,
|
||||
error_status: Optional[int] = None,
|
||||
error_data: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Update an existing task."""
|
||||
if not user_id:
|
||||
logger.error(f"[VideoStudio] Cannot update task {task_id} without user_id")
|
||||
@@ -74,6 +85,13 @@ class TaskManager:
|
||||
task.result = result
|
||||
if error:
|
||||
task.error = error
|
||||
if error_status is not None or error_data is not None:
|
||||
result_payload = task.result if isinstance(task.result, dict) else {}
|
||||
if error_status is not None:
|
||||
result_payload["error_status"] = error_status
|
||||
if error_data is not None:
|
||||
result_payload["error_data"] = error_data
|
||||
task.result = result_payload
|
||||
if progress is not None:
|
||||
task.progress = progress
|
||||
if message:
|
||||
@@ -107,7 +125,7 @@ class TaskManager:
|
||||
if status_val == "processing":
|
||||
status_val = "running"
|
||||
|
||||
return {
|
||||
response = {
|
||||
"task_id": task.task_id,
|
||||
"status": status_val,
|
||||
"result": task.result,
|
||||
@@ -117,6 +135,12 @@ class TaskManager:
|
||||
"created_at": task.created_at,
|
||||
"updated_at": task.updated_at
|
||||
}
|
||||
if isinstance(task.result, dict):
|
||||
if task.result.get("error_status") is not None:
|
||||
response["error_status"] = task.result.get("error_status")
|
||||
if task.result.get("error_data") is not None:
|
||||
response["error_data"] = task.result.get("error_data")
|
||||
return response
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,6 +4,10 @@ import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
WORKSPACE_DIR = ROOT_DIR / "workspace"
|
||||
|
||||
def migrate_database(db_path):
|
||||
"""Add missing columns to daily_workflow_plans table."""
|
||||
if not os.path.exists(db_path):
|
||||
@@ -46,14 +50,14 @@ def migrate_database(db_path):
|
||||
|
||||
def find_and_migrate_databases():
|
||||
"""Find all databases and apply migrations."""
|
||||
workspace_dir = r'c:\Users\diksha rawat\Desktop\ALwrity\workspace'
|
||||
workspace_dir = WORKSPACE_DIR
|
||||
|
||||
if not os.path.exists(workspace_dir):
|
||||
if not workspace_dir.exists():
|
||||
print(f"Workspace directory not found: {workspace_dir}")
|
||||
return
|
||||
|
||||
# Find all .db files
|
||||
db_files = list(Path(workspace_dir).glob('**/db/*.db'))
|
||||
db_files = list(workspace_dir.glob('**/db/*.db'))
|
||||
|
||||
if not db_files:
|
||||
print("No databases found to migrate")
|
||||
|
||||
@@ -53,6 +53,39 @@ WORKSPACE_DIR = os.path.join(ROOT_DIR, 'workspace')
|
||||
# Engine cache for multi-tenant support
|
||||
_user_engines = {}
|
||||
|
||||
|
||||
def _ensure_daily_workflow_schema(engine, user_id: str) -> None:
|
||||
"""Backfill required daily_workflow_plans columns for legacy tenant DBs."""
|
||||
required_columns = {
|
||||
"generation_mode": "VARCHAR(30) NOT NULL DEFAULT 'llm_generation'",
|
||||
"committee_agent_count": "INTEGER NOT NULL DEFAULT 0",
|
||||
"fallback_used": "BOOLEAN NOT NULL DEFAULT 0",
|
||||
"generation_run_id": "INTEGER",
|
||||
}
|
||||
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
table_check = conn.exec_driver_sql(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='daily_workflow_plans'"
|
||||
).fetchone()
|
||||
if not table_check:
|
||||
return
|
||||
|
||||
existing_cols = {
|
||||
row[1] for row in conn.exec_driver_sql("PRAGMA table_info(daily_workflow_plans)").fetchall()
|
||||
}
|
||||
|
||||
for col_name, col_def in required_columns.items():
|
||||
if col_name not in existing_cols:
|
||||
conn.exec_driver_sql(
|
||||
f"ALTER TABLE daily_workflow_plans ADD COLUMN {col_name} {col_def}"
|
||||
)
|
||||
logger.warning(
|
||||
f"Auto-migrated daily_workflow_plans column '{col_name}' for user {user_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed daily_workflow_plans schema compatibility check for user {user_id}: {e}")
|
||||
|
||||
def get_user_db_path(user_id: str) -> str:
|
||||
"""Get the database path for a specific user."""
|
||||
# Sanitize user_id to be safe for filesystem
|
||||
@@ -192,6 +225,7 @@ def init_user_database(user_id: str):
|
||||
UserBusinessInfoBase.metadata.create_all(bind=engine)
|
||||
ContentAssetBase.metadata.create_all(bind=engine)
|
||||
BingAnalyticsBase.metadata.create_all(bind=engine)
|
||||
_ensure_daily_workflow_schema(engine, user_id)
|
||||
|
||||
# Initialize default data for new databases
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,10 @@ Task Scheduler Package
|
||||
Modular, pluggable scheduler for ALwrity tasks.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from .core.scheduler import TaskScheduler
|
||||
from .core.executor_interface import TaskExecutor, TaskExecutionResult
|
||||
@@ -32,6 +35,7 @@ from .utils.platform_insights_task_loader import load_due_platform_insights_task
|
||||
from .utils.advertools_task_loader import load_due_advertools_tasks
|
||||
from .utils.sif_indexing_task_loader import load_due_sif_indexing_tasks
|
||||
from .utils.market_trends_task_loader import load_due_market_trends_tasks
|
||||
from services.today_workflow_service import generate_scheduled_daily_workflows
|
||||
|
||||
# Global scheduler instance (initialized on first access)
|
||||
_scheduler_instance: TaskScheduler = None
|
||||
@@ -143,6 +147,18 @@ def get_scheduler() -> TaskScheduler:
|
||||
market_trends_executor,
|
||||
load_due_market_trends_tasks
|
||||
)
|
||||
|
||||
today_workflow_hour_utc = int(os.getenv('TODAY_WORKFLOW_SCHEDULE_HOUR_UTC', '2'))
|
||||
today_workflow_minute_utc = int(os.getenv('TODAY_WORKFLOW_SCHEDULE_MINUTE_UTC', '0'))
|
||||
_scheduler_instance.scheduler.add_job(
|
||||
generate_scheduled_daily_workflows,
|
||||
trigger=CronTrigger(hour=today_workflow_hour_utc, minute=today_workflow_minute_utc, timezone='UTC'),
|
||||
id='generate_daily_workflows',
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
|
||||
return _scheduler_instance
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
|
||||
from models.agent_activity_models import AgentAlert
|
||||
from services.agent_activity_service import AgentActivityService, build_agent_event_payload
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.database import get_all_user_ids, get_session_for_user
|
||||
from loguru import logger
|
||||
|
||||
PILLAR_IDS = ["plan", "generate", "publish", "analyze", "engage", "remarket"]
|
||||
@@ -604,7 +605,12 @@ async def generate_agent_enhanced_plan(
|
||||
return result
|
||||
|
||||
|
||||
async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Optional[str] = None) -> tuple[DailyWorkflowPlan, bool]:
|
||||
async def get_or_create_daily_workflow_plan(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
date: Optional[str] = None,
|
||||
creation_source: str = "manual",
|
||||
) -> tuple[DailyWorkflowPlan, bool]:
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
date_str = date or _today_date_str()
|
||||
@@ -646,7 +652,10 @@ async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Opt
|
||||
plan = DailyWorkflowPlan(
|
||||
user_id=user_id,
|
||||
date=date_str,
|
||||
source="agent",
|
||||
source=creation_source,
|
||||
generation_mode=_derive_generation_mode(plan_data),
|
||||
committee_agent_count=_count_committee_agents(tasks),
|
||||
fallback_used=_plan_uses_fallback(tasks),
|
||||
plan_json=plan_data,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
@@ -685,6 +694,80 @@ async def get_or_create_daily_workflow_plan(db: Session, user_id: str, date: Opt
|
||||
return plan, True
|
||||
|
||||
|
||||
def _derive_generation_mode(plan_data: Dict[str, Any]) -> str:
|
||||
tasks = plan_data.get("tasks", []) if isinstance(plan_data, dict) else []
|
||||
source_modes = set()
|
||||
for task in tasks:
|
||||
metadata = task.get("metadata") if isinstance(task, dict) else {}
|
||||
metadata = metadata if isinstance(metadata, dict) else {}
|
||||
source_agent = str(metadata.get("source_agent") or "").strip()
|
||||
source = str(metadata.get("source") or "").strip()
|
||||
if source_agent:
|
||||
source_modes.add("agent_committee")
|
||||
elif source in {"controlled_fallback", "llm_pillar_backfill"}:
|
||||
source_modes.add(source)
|
||||
|
||||
if "agent_committee" in source_modes:
|
||||
return "agent_committee"
|
||||
if "controlled_fallback" in source_modes:
|
||||
return "controlled_fallback"
|
||||
if "llm_pillar_backfill" in source_modes:
|
||||
return "llm_pillar_backfill"
|
||||
return "llm_generation"
|
||||
|
||||
|
||||
def _count_committee_agents(tasks: List[Dict[str, Any]]) -> int:
|
||||
agents = set()
|
||||
for task in tasks:
|
||||
metadata = task.get("metadata") if isinstance(task, dict) else {}
|
||||
metadata = metadata if isinstance(metadata, dict) else {}
|
||||
source_agent = str(metadata.get("source_agent") or "").strip()
|
||||
if source_agent:
|
||||
agents.add(source_agent)
|
||||
return len(agents)
|
||||
|
||||
|
||||
def _plan_uses_fallback(tasks: List[Dict[str, Any]]) -> bool:
|
||||
for task in tasks:
|
||||
metadata = task.get("metadata") if isinstance(task, dict) else {}
|
||||
metadata = metadata if isinstance(metadata, dict) else {}
|
||||
source = str(metadata.get("source") or "").strip()
|
||||
if source in {"controlled_fallback", "llm_pillar_backfill"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def generate_scheduled_daily_workflows() -> Dict[str, int]:
|
||||
user_ids = get_all_user_ids()
|
||||
stats = {"users_seen": 0, "created": 0, "existing": 0, "failed": 0}
|
||||
|
||||
for user_id in user_ids:
|
||||
stats["users_seen"] += 1
|
||||
db = None
|
||||
try:
|
||||
db = get_session_for_user(user_id)
|
||||
plan, created = await get_or_create_daily_workflow_plan(
|
||||
db,
|
||||
user_id,
|
||||
creation_source="scheduled",
|
||||
)
|
||||
if created:
|
||||
stats["created"] += 1
|
||||
logger.info("Scheduled daily workflow created for user {} date {}", user_id, plan.date)
|
||||
else:
|
||||
stats["existing"] += 1
|
||||
logger.info("Scheduled daily workflow already exists for user {} date {}", user_id, plan.date)
|
||||
except Exception as e:
|
||||
stats["failed"] += 1
|
||||
logger.error("Scheduled daily workflow generation failed for user {}: {}", user_id, e)
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
logger.info("Scheduled daily workflow run complete: {}", stats)
|
||||
return stats
|
||||
|
||||
|
||||
def update_task_status(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
|
||||
@@ -3,6 +3,7 @@ Video generation operations (text-to-video and image-to-video).
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -12,6 +13,19 @@ from .base import VideoBase
|
||||
logger = get_service_logger("wavespeed.generators.video.generation")
|
||||
|
||||
|
||||
def _extract_wavespeed_message(response_text: str) -> str:
|
||||
"""Best-effort extraction of WaveSpeed error message from response payload."""
|
||||
if not response_text:
|
||||
return ""
|
||||
try:
|
||||
parsed = json.loads(response_text)
|
||||
if isinstance(parsed, dict):
|
||||
return str(parsed.get("message") or parsed.get("error") or "")
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
class VideoGeneration(VideoBase):
|
||||
"""Video generation operations."""
|
||||
|
||||
@@ -31,6 +45,25 @@ class VideoGeneration(VideoBase):
|
||||
response = requests.post(url, headers=self._get_headers(), json=payload, timeout=timeout)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"[WaveSpeed] Submission failed: {response.status_code} {response.text}")
|
||||
|
||||
error_message = _extract_wavespeed_message(response.text)
|
||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail={
|
||||
"error": "Insufficient WaveSpeed credits",
|
||||
"message": "Insufficient credits. Please top up to continue video generation.",
|
||||
"provider": "wavespeed",
|
||||
"usage_info": {
|
||||
"provider": "wavespeed",
|
||||
"type": "credits",
|
||||
"limit_type": "provider_credits",
|
||||
"operation_type": "scene_animation",
|
||||
"action_required": "top_up",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
@@ -75,6 +108,25 @@ class VideoGeneration(VideoBase):
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
||||
|
||||
error_message = _extract_wavespeed_message(response.text)
|
||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail={
|
||||
"error": "Insufficient WaveSpeed credits",
|
||||
"message": "Insufficient credits. Please top up to continue video generation.",
|
||||
"provider": "wavespeed",
|
||||
"usage_info": {
|
||||
"provider": "wavespeed",
|
||||
"type": "credits",
|
||||
"limit_type": "provider_credits",
|
||||
"operation_type": "video_generation",
|
||||
"action_required": "top_up",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
@@ -174,6 +226,25 @@ class VideoGeneration(VideoBase):
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
||||
|
||||
error_message = _extract_wavespeed_message(response.text)
|
||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail={
|
||||
"error": "Insufficient WaveSpeed credits",
|
||||
"message": "Insufficient credits. Please top up to continue video generation.",
|
||||
"provider": "wavespeed",
|
||||
"usage_info": {
|
||||
"provider": "wavespeed",
|
||||
"type": "credits",
|
||||
"limit_type": "provider_credits",
|
||||
"operation_type": "video_generation",
|
||||
"action_required": "top_up",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
|
||||
Reference in New Issue
Block a user