Merge remote changes and add SEO fields to Tasks model, improve content generation response handling, and enhance progress bar animation
This commit is contained in:
@@ -269,6 +269,7 @@ class AICore:
|
||||
'cost': cost,
|
||||
'error': None,
|
||||
'api_id': api_id,
|
||||
'duration': request_duration, # Add duration tracking
|
||||
}
|
||||
else:
|
||||
error_msg = 'No content in OpenAI response'
|
||||
@@ -315,8 +316,9 @@ class AICore:
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f'Unexpected error: {str(e)}'
|
||||
print(f"[AI][{function_name}][Error] {error_msg}")
|
||||
logger.error(error_msg, exc_info=True)
|
||||
logger.error(f"[AI][{function_name}][Error] {error_msg}", exc_info=True)
|
||||
if tracker:
|
||||
tracker.error('UnexpectedError', error_msg, e)
|
||||
return {
|
||||
'content': None,
|
||||
'error': error_msg,
|
||||
|
||||
@@ -34,16 +34,14 @@ class BaseAIFunction(ABC):
|
||||
def validate(self, payload: dict, account=None) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate input payload.
|
||||
Default: checks for 'ids' array, max_items limit.
|
||||
Default: checks for 'ids' array.
|
||||
Override for custom validation.
|
||||
"""
|
||||
ids = payload.get('ids', [])
|
||||
if not ids:
|
||||
return {'valid': False, 'error': 'No IDs provided'}
|
||||
|
||||
max_items = self.get_max_items()
|
||||
if max_items and len(ids) > max_items:
|
||||
return {'valid': False, 'error': f'Maximum {max_items} items allowed'}
|
||||
# Removed max_items limit check - no limits enforced
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
AI Function implementations
|
||||
"""
|
||||
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
|
||||
from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction, generate_ideas_core
|
||||
# REMOVED: generate_ideas function removed
|
||||
# from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction, generate_ideas_core
|
||||
from igny8_core.ai.functions.generate_content import GenerateContentFunction, generate_content_core
|
||||
from igny8_core.ai.functions.generate_images import GenerateImagesFunction, generate_images_core
|
||||
|
||||
__all__ = [
|
||||
'AutoClusterFunction',
|
||||
'GenerateIdeasFunction',
|
||||
'generate_ideas_core',
|
||||
# REMOVED: generate_ideas function removed
|
||||
# 'GenerateIdeasFunction',
|
||||
# 'generate_ideas_core',
|
||||
'GenerateContentFunction',
|
||||
'generate_content_core',
|
||||
'GenerateImagesFunction',
|
||||
|
||||
@@ -34,14 +34,15 @@ class AutoClusterFunction(BaseAIFunction):
|
||||
}
|
||||
|
||||
def get_max_items(self) -> int:
|
||||
return 20
|
||||
# No limit - return None
|
||||
return None
|
||||
|
||||
def validate(self, payload: dict, account=None) -> Dict:
|
||||
"""Custom validation for clustering with plan limit checks"""
|
||||
from igny8_core.ai.validators import validate_ids, validate_keywords_exist, validate_cluster_limits
|
||||
"""Custom validation for clustering"""
|
||||
from igny8_core.ai.validators import validate_ids, validate_keywords_exist
|
||||
|
||||
# Base validation
|
||||
result = validate_ids(payload, max_items=self.get_max_items())
|
||||
# Base validation (no max_items limit)
|
||||
result = validate_ids(payload, max_items=None)
|
||||
if not result['valid']:
|
||||
return result
|
||||
|
||||
@@ -51,10 +52,7 @@ class AutoClusterFunction(BaseAIFunction):
|
||||
if not keywords_result['valid']:
|
||||
return keywords_result
|
||||
|
||||
# Check plan limits
|
||||
limit_result = validate_cluster_limits(account, operation_type='cluster')
|
||||
if not limit_result['valid']:
|
||||
return limit_result
|
||||
# Removed plan limits check
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class GenerateImagesFunction(BaseAIFunction):
|
||||
}
|
||||
|
||||
def get_max_items(self) -> int:
|
||||
return 20 # Max tasks per batch
|
||||
# No limit - return None
|
||||
return None
|
||||
|
||||
def validate(self, payload: dict, account=None) -> Dict:
|
||||
"""Validate task IDs"""
|
||||
|
||||
@@ -117,7 +117,8 @@ Make sure each prompt is detailed enough for image generation, describing the vi
|
||||
# Mapping from function names to prompt types
|
||||
FUNCTION_TO_PROMPT_TYPE = {
|
||||
'auto_cluster': 'clustering',
|
||||
'generate_ideas': 'ideas',
|
||||
# REMOVED: generate_ideas function removed
|
||||
# 'generate_ideas': 'ideas',
|
||||
'generate_content': 'content_generation',
|
||||
'generate_images': 'image_prompt_extraction',
|
||||
'extract_image_prompts': 'image_prompt_extraction',
|
||||
|
||||
@@ -66,10 +66,11 @@ def _load_auto_cluster():
|
||||
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
|
||||
return AutoClusterFunction
|
||||
|
||||
def _load_generate_ideas():
|
||||
"""Lazy loader for generate_ideas function"""
|
||||
from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction
|
||||
return GenerateIdeasFunction
|
||||
# REMOVED: generate_ideas function removed
|
||||
# def _load_generate_ideas():
|
||||
# """Lazy loader for generate_ideas function"""
|
||||
# from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction
|
||||
# return GenerateIdeasFunction
|
||||
|
||||
def _load_generate_content():
|
||||
"""Lazy loader for generate_content function"""
|
||||
@@ -82,7 +83,8 @@ def _load_generate_images():
|
||||
return GenerateImagesFunction
|
||||
|
||||
register_lazy_function('auto_cluster', _load_auto_cluster)
|
||||
register_lazy_function('generate_ideas', _load_generate_ideas)
|
||||
# REMOVED: generate_ideas function removed
|
||||
# register_lazy_function('generate_ideas', _load_generate_ideas)
|
||||
register_lazy_function('generate_content', _load_generate_content)
|
||||
register_lazy_function('generate_images', _load_generate_images)
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ MODEL_CONFIG = {
|
||||
"temperature": 0.7,
|
||||
"response_format": {"type": "json_object"}, # Auto-enabled for JSON mode models
|
||||
},
|
||||
"generate_ideas": {
|
||||
"model": "gpt-4.1",
|
||||
"max_tokens": 4000,
|
||||
"temperature": 0.7,
|
||||
"response_format": {"type": "json_object"},
|
||||
},
|
||||
# REMOVED: generate_ideas function removed
|
||||
# "generate_ideas": {
|
||||
# "model": "gpt-4.1",
|
||||
# "max_tokens": 4000,
|
||||
# "temperature": 0.7,
|
||||
# "response_format": {"type": "json_object"},
|
||||
# },
|
||||
"generate_content": {
|
||||
"model": "gpt-4.1",
|
||||
"max_tokens": 8000,
|
||||
@@ -40,7 +41,8 @@ MODEL_CONFIG = {
|
||||
FUNCTION_ALIASES = {
|
||||
"cluster_keywords": "auto_cluster",
|
||||
"auto_cluster_keywords": "auto_cluster",
|
||||
"auto_generate_ideas": "generate_ideas",
|
||||
# REMOVED: generate_ideas function removed
|
||||
# "auto_generate_ideas": "generate_ideas",
|
||||
"auto_generate_content": "generate_content",
|
||||
"auto_generate_images": "generate_images",
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8.settings')
|
||||
django.setup()
|
||||
|
||||
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
|
||||
from igny8_core.ai.functions.generate_ideas import generate_ideas_core
|
||||
# REMOVED: generate_ideas function removed
|
||||
# from igny8_core.ai.functions.generate_ideas import generate_ideas_core
|
||||
from igny8_core.ai.functions.generate_content import generate_content_core
|
||||
from igny8_core.ai.functions.generate_images import generate_images_core
|
||||
from igny8_core.ai.ai_core import AICore
|
||||
@@ -122,7 +123,8 @@ if __name__ == '__main__':
|
||||
test_ai_core()
|
||||
test_json_extraction()
|
||||
test_auto_cluster()
|
||||
test_generate_ideas()
|
||||
# REMOVED: generate_ideas function removed
|
||||
# test_generate_ideas()
|
||||
test_generate_content()
|
||||
test_generate_images()
|
||||
|
||||
|
||||
@@ -236,21 +236,40 @@ class ConsoleStepTracker:
|
||||
self.start_time = time.time()
|
||||
self.steps = []
|
||||
self.current_phase = None
|
||||
|
||||
# Debug: Verify DEBUG_MODE is enabled
|
||||
import sys
|
||||
if DEBUG_MODE:
|
||||
init_msg = f"[DEBUG] ConsoleStepTracker initialized for '{function_name}' - DEBUG_MODE is ENABLED"
|
||||
logger.info(init_msg)
|
||||
print(init_msg, flush=True, file=sys.stdout)
|
||||
else:
|
||||
init_msg = f"[WARNING] ConsoleStepTracker initialized for '{function_name}' - DEBUG_MODE is DISABLED"
|
||||
logger.warning(init_msg)
|
||||
print(init_msg, flush=True, file=sys.stdout)
|
||||
|
||||
def _log(self, phase: str, message: str, status: str = 'info'):
|
||||
"""Internal logging method that checks DEBUG_MODE"""
|
||||
if not DEBUG_MODE:
|
||||
return
|
||||
|
||||
import sys
|
||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||
phase_label = phase.upper()
|
||||
|
||||
if status == 'error':
|
||||
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] [ERROR] {message}")
|
||||
log_msg = f"[{timestamp}] [{self.function_name}] [{phase_label}] [ERROR] {message}"
|
||||
# Use logger.error for errors so they're always visible
|
||||
logger.error(log_msg)
|
||||
elif status == 'success':
|
||||
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] ✅ {message}")
|
||||
log_msg = f"[{timestamp}] [{self.function_name}] [{phase_label}] ✅ {message}"
|
||||
logger.info(log_msg)
|
||||
else:
|
||||
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] {message}")
|
||||
log_msg = f"[{timestamp}] [{self.function_name}] [{phase_label}] {message}"
|
||||
logger.info(log_msg)
|
||||
|
||||
# Also print to stdout for immediate visibility (works in Celery worker logs)
|
||||
print(log_msg, flush=True, file=sys.stdout)
|
||||
|
||||
self.steps.append({
|
||||
'timestamp': timestamp,
|
||||
@@ -285,7 +304,10 @@ class ConsoleStepTracker:
|
||||
duration = time.time() - self.start_time
|
||||
self._log('DONE', f"{message} (Duration: {duration:.2f}s)", status='success')
|
||||
if DEBUG_MODE:
|
||||
print(f"[{self.function_name}] === AI Task Complete ===")
|
||||
import sys
|
||||
complete_msg = f"[{self.function_name}] === AI Task Complete ==="
|
||||
logger.info(complete_msg)
|
||||
print(complete_msg, flush=True, file=sys.stdout)
|
||||
|
||||
def error(self, error_type: str, message: str, exception: Exception = None):
|
||||
"""Log error with standardized format"""
|
||||
@@ -294,9 +316,12 @@ class ConsoleStepTracker:
|
||||
error_msg += f" ({type(exception).__name__})"
|
||||
self._log(self.current_phase or 'ERROR', error_msg, status='error')
|
||||
if DEBUG_MODE and exception:
|
||||
import sys
|
||||
import traceback
|
||||
print(f"[{self.function_name}] [ERROR] Stack trace:")
|
||||
traceback.print_exc()
|
||||
error_trace_msg = f"[{self.function_name}] [ERROR] Stack trace:"
|
||||
logger.error(error_trace_msg, exc_info=exception)
|
||||
print(error_trace_msg, flush=True, file=sys.stdout)
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
|
||||
def retry(self, attempt: int, max_attempts: int, reason: str = ""):
|
||||
"""Log retry attempt"""
|
||||
|
||||
@@ -10,11 +10,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def validate_ids(payload: dict, max_items: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Base validation: checks for 'ids' array and max_items limit.
|
||||
Base validation: checks for 'ids' array.
|
||||
|
||||
Args:
|
||||
payload: Request payload containing 'ids' array
|
||||
max_items: Maximum number of items allowed (None = no limit)
|
||||
max_items: Maximum number of items allowed (deprecated - no longer enforced)
|
||||
|
||||
Returns:
|
||||
Dict with 'valid' (bool) and optional 'error' (str)
|
||||
@@ -23,8 +23,7 @@ def validate_ids(payload: dict, max_items: Optional[int] = None) -> Dict[str, An
|
||||
if not ids:
|
||||
return {'valid': False, 'error': 'No IDs provided'}
|
||||
|
||||
if max_items and len(ids) > max_items:
|
||||
return {'valid': False, 'error': f'Maximum {max_items} items allowed'}
|
||||
# Removed max_items limit check - no limits enforced
|
||||
|
||||
return {'valid': True}
|
||||
|
||||
@@ -55,46 +54,16 @@ def validate_keywords_exist(ids: list, account=None) -> Dict[str, Any]:
|
||||
def validate_cluster_limits(account, operation_type: str = 'cluster') -> Dict[str, Any]:
|
||||
"""
|
||||
Validate plan limits for cluster operations.
|
||||
DISABLED: All limits have been removed.
|
||||
|
||||
Args:
|
||||
account: Account object
|
||||
operation_type: Type of operation ('cluster', 'idea', etc.)
|
||||
|
||||
Returns:
|
||||
Dict with 'valid' (bool) and optional 'error' (str)
|
||||
Dict with 'valid' (bool) - always returns valid
|
||||
"""
|
||||
if not account:
|
||||
return {'valid': False, 'error': 'Account is required'}
|
||||
|
||||
plan = getattr(account, 'plan', None)
|
||||
if not plan:
|
||||
return {'valid': False, 'error': 'Account does not have an active plan'}
|
||||
|
||||
if operation_type == 'cluster':
|
||||
from igny8_core.modules.planner.models import Clusters
|
||||
|
||||
# Check daily cluster limit
|
||||
now = timezone.now()
|
||||
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
clusters_today = Clusters.objects.filter(
|
||||
account=account,
|
||||
created_at__gte=start_of_day
|
||||
).count()
|
||||
|
||||
if plan.daily_cluster_limit and clusters_today >= plan.daily_cluster_limit:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Daily cluster limit reached ({plan.daily_cluster_limit} clusters per day). Please try again tomorrow.'
|
||||
}
|
||||
|
||||
# Check max clusters limit
|
||||
total_clusters = Clusters.objects.filter(account=account).count()
|
||||
if plan.max_clusters and total_clusters >= plan.max_clusters:
|
||||
return {
|
||||
'valid': False,
|
||||
'error': f'Maximum cluster limit reached ({plan.max_clusters} clusters). Please upgrade your plan or delete existing clusters.'
|
||||
}
|
||||
|
||||
# All limits removed - always return valid
|
||||
return {'valid': True}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user