Stage 3 - AI refactor

This commit is contained in:
alorig
2025-11-09 19:30:22 +05:00
parent 375473308d
commit c04c688aa0
6 changed files with 333 additions and 29 deletions

View File

@@ -17,7 +17,9 @@ from .constants import (
IMAGE_MODEL_RATES,
VALID_OPENAI_IMAGE_MODELS,
VALID_SIZES_BY_MODEL,
DEBUG_MODE,
)
from .tracker import ConsoleStepTracker
logger = logging.getLogger(__name__)
@@ -100,7 +102,8 @@ class AICore:
temperature: float = 0.7,
response_format: Optional[Dict] = None,
api_key: Optional[str] = None,
function_name: str = 'ai_request'
function_name: str = 'ai_request',
tracker: Optional[ConsoleStepTracker] = None
) -> Dict[str, Any]:
"""
Centralized AI request handler with console logging.
@@ -114,18 +117,23 @@ class AICore:
response_format: Optional response format dict (for JSON mode)
api_key: Optional API key override
function_name: Function name for logging (e.g., 'cluster_keywords')
tracker: Optional ConsoleStepTracker instance for logging
Returns:
Dict with 'content', 'input_tokens', 'output_tokens', 'total_tokens',
'model', 'cost', 'error', 'api_id'
"""
print(f"[AI][{function_name}] Step 1: Preparing request...")
# Use provided tracker or create a new one
if tracker is None:
tracker = ConsoleStepTracker(function_name)
tracker.ai_call("Preparing request...")
# Step 1: Validate API key
api_key = api_key or self._openai_api_key
if not api_key:
error_msg = 'OpenAI API key not configured'
print(f"[AI][{function_name}][Error] {error_msg}")
tracker.error('ConfigurationError', error_msg)
return {
'content': None,
'error': error_msg,
@@ -139,20 +147,20 @@ class AICore:
# Step 2: Determine model
active_model = model or self._default_model
print(f"[AI][{function_name}] Step 2: Using model: {active_model}")
tracker.ai_call(f"Using model: {active_model}")
# Step 3: Auto-enable JSON mode for supported models
if response_format is None and active_model in JSON_MODE_MODELS:
response_format = {'type': 'json_object'}
print(f"[AI][{function_name}] Step 3: Auto-enabled JSON mode for {active_model}")
tracker.ai_call(f"Auto-enabled JSON mode for {active_model}")
elif response_format:
print(f"[AI][{function_name}] Step 3: Using custom response format: {response_format}")
tracker.ai_call(f"Using custom response format: {response_format}")
else:
print(f"[AI][{function_name}] Step 3: Using text response format")
tracker.ai_call("Using text response format")
# Step 4: Validate prompt length
prompt_length = len(prompt)
print(f"[AI][{function_name}] Step 4: Prompt length: {prompt_length} characters")
tracker.ai_call(f"Prompt length: {prompt_length} characters")
# Step 5: Build request payload
url = 'https://api.openai.com/v1/chat/completions'
@@ -173,16 +181,16 @@ class AICore:
if response_format:
body_data['response_format'] = response_format
print(f"[AI][{function_name}] Step 5: Request payload prepared (model={active_model}, max_tokens={max_tokens}, temp={temperature})")
tracker.ai_call(f"Request payload prepared (model={active_model}, max_tokens={max_tokens}, temp={temperature})")
# Step 6: Send request
print(f"[AI][{function_name}] Step 6: Sending request to OpenAI API...")
tracker.ai_call("Sending request to OpenAI API...")
request_start = time.time()
try:
response = requests.post(url, headers=headers, json=body_data, timeout=60)
request_duration = time.time() - request_start
print(f"[AI][{function_name}] Step 7: Received response in {request_duration:.2f}s (status={response.status_code})")
tracker.ai_call(f"Received response in {request_duration:.2f}s (status={response.status_code})")
# Step 7: Validate HTTP response
if response.status_code != 200:
@@ -196,10 +204,11 @@ class AICore:
# Check for rate limit
if response.status_code == 429:
retry_after = response.headers.get('retry-after', '60')
print(f"[AI][{function_name}][Error] OpenAI Rate Limit - waiting {retry_after}s")
error_message += f" (Rate limit - retry after {retry_after}s)"
tracker.rate_limit(retry_after)
error_message += f" (Rate limit - retry after {retry_after}s)")
else:
tracker.error('HTTPError', error_message)
print(f"[AI][{function_name}][Error] {error_message}")
logger.error(f"OpenAI API HTTP error {response.status_code}: {error_message}")
return {
@@ -218,7 +227,7 @@ class AICore:
data = response.json()
except json.JSONDecodeError as e:
error_msg = f'Failed to parse JSON response: {str(e)}'
print(f"[AI][{function_name}][Error] {error_msg}")
tracker.malformed_json(str(e))
logger.error(error_msg)
return {
'content': None,
@@ -241,15 +250,15 @@ class AICore:
output_tokens = usage.get('completion_tokens', 0)
total_tokens = usage.get('total_tokens', 0)
print(f"[AI][{function_name}] Step 8: Received {total_tokens} tokens (input: {input_tokens}, output: {output_tokens})")
print(f"[AI][{function_name}] Step 9: Content length: {len(content)} characters")
tracker.parse(f"Received {total_tokens} tokens (input: {input_tokens}, output: {output_tokens})")
tracker.parse(f"Content length: {len(content)} characters")
# Step 10: Calculate cost
rates = MODEL_RATES.get(active_model, {'input': 2.00, 'output': 8.00})
cost = (input_tokens * rates['input'] + output_tokens * rates['output']) / 1_000_000
print(f"[AI][{function_name}] Step 10: Cost calculated: ${cost:.6f}")
tracker.parse(f"Cost calculated: ${cost:.6f}")
print(f"[AI][{function_name}][Success] Request completed successfully")
tracker.done("Request completed successfully")
return {
'content': content,
@@ -263,7 +272,7 @@ class AICore:
}
else:
error_msg = 'No content in OpenAI response'
print(f"[AI][{function_name}][Error] {error_msg}")
tracker.error('EmptyResponse', error_msg)
logger.error(error_msg)
return {
'content': None,
@@ -278,7 +287,7 @@ class AICore:
except requests.exceptions.Timeout:
error_msg = 'Request timeout (60s exceeded)'
print(f"[AI][{function_name}][Error] {error_msg}")
tracker.timeout(60)
logger.error(error_msg)
return {
'content': None,
@@ -292,7 +301,7 @@ class AICore:
}
except requests.exceptions.RequestException as e:
error_msg = f'Request exception: {str(e)}'
print(f"[AI][{function_name}][Error] {error_msg}")
tracker.error('RequestException', error_msg, e)
logger.error(f"OpenAI API error: {error_msg}", exc_info=True)
return {
'content': None,

View File

@@ -35,3 +35,7 @@ DEFAULT_AI_MODEL = 'gpt-4.1'
# JSON mode supported models
JSON_MODE_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo-preview']
# Debug mode - controls console logging
# Set to False in production to disable verbose logging
DEBUG_MODE = True

View File

@@ -11,6 +11,7 @@ from igny8_core.modules.planner.models import Clusters, ContentIdeas
from igny8_core.modules.system.utils import get_prompt_value
from igny8_core.ai.ai_core import AICore
from igny8_core.ai.validators import validate_cluster_exists, validate_cluster_limits
from igny8_core.ai.tracker import ConsoleStepTracker
logger = logging.getLogger(__name__)
@@ -195,6 +196,9 @@ def generate_ideas_core(cluster_id: int, account_id: int = None, progress_callba
Returns:
Dict with 'success', 'idea_created', 'message', etc.
"""
tracker = ConsoleStepTracker('generate_ideas')
tracker.init("Task started")
try:
from igny8_core.auth.models import Account
@@ -202,6 +206,8 @@ def generate_ideas_core(cluster_id: int, account_id: int = None, progress_callba
if account_id:
account = Account.objects.get(id=account_id)
tracker.prep("Loading account and cluster data...")
# Use the new function class
fn = GenerateIdeasFunction()
# Store account for use in methods
@@ -211,14 +217,18 @@ def generate_ideas_core(cluster_id: int, account_id: int = None, progress_callba
payload = {'ids': [cluster_id]}
# Validate
tracker.prep("Validating input...")
validated = fn.validate(payload, account)
if not validated['valid']:
tracker.error('ValidationError', validated['error'])
return {'success': False, 'error': validated['error']}
# Prepare data
tracker.prep("Loading cluster with keywords...")
data = fn.prepare(payload, account)
# Build prompt
tracker.prep("Building prompt...")
prompt = fn.build_prompt(data, account)
# Call AI using centralized request handler
@@ -226,23 +236,32 @@ def generate_ideas_core(cluster_id: int, account_id: int = None, progress_callba
result = ai_core.run_ai_request(
prompt=prompt,
max_tokens=4000,
function_name='generate_ideas'
function_name='generate_ideas',
tracker=tracker
)
if result.get('error'):
return {'success': False, 'error': result['error']}
# Parse response
tracker.parse("Parsing AI response...")
ideas_data = fn.parse_response(result['content'])
if not ideas_data:
tracker.error('ParseError', 'No ideas generated by AI')
return {'success': False, 'error': 'No ideas generated by AI'}
tracker.parse(f"Parsed {len(ideas_data)} idea(s)")
# Take first idea
idea_data = ideas_data[0]
# Save output
tracker.save("Saving idea to database...")
save_result = fn.save_output(ideas_data, data, account)
tracker.save(f"Saved {save_result['ideas_created']} idea(s)")
tracker.done(f"Idea '{idea_data.get('title', 'Untitled')}' created successfully")
return {
'success': True,
@@ -251,6 +270,7 @@ def generate_ideas_core(cluster_id: int, account_id: int = None, progress_callba
}
except Exception as e:
tracker.error('Exception', str(e), e)
logger.error(f"Error in generate_ideas_core: {str(e)}", exc_info=True)
return {'success': False, 'error': str(e)}

View File

@@ -4,7 +4,9 @@ Progress and Step Tracking utilities for AI framework
import time
import logging
from typing import List, Dict, Any, Optional, Callable
from datetime import datetime
from igny8_core.ai.types import StepLog, ProgressState
from igny8_core.ai.constants import DEBUG_MODE
logger = logging.getLogger(__name__)
@@ -221,3 +223,100 @@ class CostTracker:
"""Get all operations"""
return self.operations
class ConsoleStepTracker:
"""
Lightweight console-based step tracker for AI functions.
Logs each step to console with timestamps and clear labels.
Only logs if DEBUG_MODE is True.
"""
def __init__(self, function_name: str):
self.function_name = function_name
self.start_time = time.time()
self.steps = []
self.current_phase = None
def _log(self, phase: str, message: str, status: str = 'info'):
"""Internal logging method that checks DEBUG_MODE"""
if not DEBUG_MODE:
return
timestamp = datetime.now().strftime('%H:%M:%S')
phase_label = phase.upper()
if status == 'error':
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] [ERROR] {message}")
elif status == 'success':
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] ✅ {message}")
else:
print(f"[{timestamp}] [{self.function_name}] [{phase_label}] {message}")
self.steps.append({
'timestamp': timestamp,
'phase': phase,
'message': message,
'status': status
})
self.current_phase = phase
def init(self, message: str = "Task started"):
"""Log initialization phase"""
self._log('INIT', message)
def prep(self, message: str):
"""Log preparation phase"""
self._log('PREP', message)
def ai_call(self, message: str):
"""Log AI call phase"""
self._log('AI_CALL', message)
def parse(self, message: str):
"""Log parsing phase"""
self._log('PARSE', message)
def save(self, message: str):
"""Log save phase"""
self._log('SAVE', message)
def done(self, message: str = "Execution completed"):
"""Log completion"""
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 ===")
def error(self, error_type: str, message: str, exception: Exception = None):
"""Log error with standardized format"""
error_msg = f"{error_type} {message}"
if exception:
error_msg += f" ({type(exception).__name__})"
self._log(self.current_phase or 'ERROR', error_msg, status='error')
if DEBUG_MODE and exception:
import traceback
print(f"[{self.function_name}] [ERROR] Stack trace:")
traceback.print_exc()
def retry(self, attempt: int, max_attempts: int, reason: str = ""):
"""Log retry attempt"""
msg = f"Retry attempt {attempt}/{max_attempts}"
if reason:
msg += f" {reason}"
self._log('AI_CALL', msg, status='info')
def timeout(self, timeout_seconds: int):
"""Log timeout"""
self.error('Timeout', f"Request timeout after {timeout_seconds}s")
def rate_limit(self, retry_after: str):
"""Log rate limit"""
self.error('RateLimit', f"OpenAI rate limit hit, retry in {retry_after}s")
def malformed_json(self, details: str = ""):
"""Log JSON parsing error"""
msg = "Failed to parse model response: Unexpected JSON"
if details:
msg += f" {details}"
self.error('MalformedJSON', msg)