automation and ai and some planning and fixes adn docs reorg

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-29 01:41:36 +00:00
parent 748de099dd
commit 53fdebf733
20 changed files with 927 additions and 4288 deletions

View File

@@ -87,13 +87,13 @@ class AICore:
response_format: Optional[Dict] = None,
api_key: Optional[str] = None,
function_name: str = 'ai_request',
function_id: Optional[str] = None,
prompt_prefix: Optional[str] = None,
tracker: Optional[ConsoleStepTracker] = None
) -> Dict[str, Any]:
"""
Centralized AI request handler with console logging.
All AI text generation requests go through this method.
Args:
prompt: Prompt text
model: Model name (required - must be provided from IntegrationSettings)
@@ -102,12 +102,13 @@ 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')
prompt_prefix: Optional prefix to add before prompt (e.g., '##GP01-Clustering')
tracker: Optional ConsoleStepTracker instance for logging
Returns:
Dict with 'content', 'input_tokens', 'output_tokens', 'total_tokens',
'model', 'cost', 'error', 'api_id'
Raises:
ValueError: If model is not provided
"""
@@ -184,16 +185,16 @@ class AICore:
else:
tracker.ai_call("Using text response format")
# Step 4: Validate prompt length and add function_id
# Step 4: Validate prompt length and add prompt_prefix
prompt_length = len(prompt)
tracker.ai_call(f"Prompt length: {prompt_length} characters")
# Add function_id to prompt if provided (for tracking)
# Add prompt_prefix to prompt if provided (for tracking)
# Format: ##GP01-Clustering or ##CP01-Clustering
final_prompt = prompt
if function_id:
function_id_prefix = f'function_id: "{function_id}"\n\n'
final_prompt = function_id_prefix + prompt
tracker.ai_call(f"Added function_id to prompt: {function_id}")
if prompt_prefix:
final_prompt = f'{prompt_prefix}\n\n{prompt}'
tracker.ai_call(f"Added prompt prefix: {prompt_prefix}")
# Step 5: Build request payload
url = 'https://api.openai.com/v1/chat/completions'

View File

@@ -306,12 +306,13 @@ class AIEngine:
ai_core = AICore(account=self.account)
function_name = fn.get_name()
# Generate function_id for tracking (ai-{function_name}-01)
# Normalize underscores to hyphens to match frontend tracking IDs
function_id_base = function_name.replace('_', '-')
function_id = f"ai-{function_id_base}-01-desktop"
# Generate prompt prefix for tracking (e.g., ##GP01-Clustering or ##CP01-Clustering)
# This replaces function_id and indicates whether prompt is global or custom
from igny8_core.ai.prompts import get_prompt_prefix_for_function
prompt_prefix = get_prompt_prefix_for_function(function_name, account=self.account)
logger.info(f"[AIEngine] Using prompt prefix: {prompt_prefix}")
# Get model config from settings (requires account)
# This will raise ValueError if IntegrationSettings not configured
try:
@@ -349,7 +350,7 @@ class AIEngine:
temperature=model_config.get('temperature'),
response_format=model_config.get('response_format'),
function_name=function_name,
function_id=function_id # Pass function_id for tracking
prompt_prefix=prompt_prefix # Pass prompt prefix for tracking (replaces function_id)
)
except Exception as e:
error_msg = f"AI call failed: {str(e)}"

View File

@@ -3,7 +3,7 @@ Prompt Registry - Centralized prompt management with override hierarchy
Supports: task-level overrides → DB prompts → GlobalAIPrompt (REQUIRED)
"""
import logging
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Tuple
from django.db import models
logger = logging.getLogger(__name__)
@@ -16,10 +16,10 @@ class PromptRegistry:
2. DB prompt for (account, function)
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
"""
# Removed ALL hardcoded prompts - GlobalAIPrompt is now the ONLY source of default prompts
# To add/modify prompts, use Django admin: /admin/system/globalaiprompt/
# Mapping from function names to prompt types
FUNCTION_TO_PROMPT_TYPE = {
'auto_cluster': 'clustering',
@@ -35,7 +35,114 @@ class PromptRegistry:
'generate_service_page': 'service_generation',
'generate_taxonomy': 'taxonomy_generation',
}
# Mapping of prompt types to their prefix numbers and display names
# Format: {prompt_type: (number, display_name)}
# GP = Global Prompt, CP = Custom Prompt
PROMPT_PREFIX_MAP = {
'clustering': ('01', 'Clustering'),
'ideas': ('02', 'Ideas'),
'content_generation': ('03', 'ContentGen'),
'image_prompt_extraction': ('04', 'ImagePrompts'),
'site_structure_generation': ('05', 'SiteStructure'),
'optimize_content': ('06', 'OptimizeContent'),
'product_generation': ('07', 'ProductGen'),
'service_generation': ('08', 'ServiceGen'),
'taxonomy_generation': ('09', 'TaxonomyGen'),
'image_prompt_template': ('10', 'ImageTemplate'),
'negative_prompt': ('11', 'NegativePrompt'),
}
@classmethod
def get_prompt_prefix(cls, prompt_type: str, is_custom: bool) -> str:
"""
Generate prompt prefix for tracking.
Args:
prompt_type: The prompt type (e.g., 'clustering', 'ideas')
is_custom: True if using custom/account-specific prompt, False if global
Returns:
Prefix string like "##GP01-Clustering" or "##CP01-Clustering"
"""
prefix_info = cls.PROMPT_PREFIX_MAP.get(prompt_type, ('00', prompt_type.title()))
number, display_name = prefix_info
prefix_type = 'CP' if is_custom else 'GP'
return f"##{prefix_type}{number}-{display_name}"
@classmethod
def get_prompt_with_metadata(
cls,
function_name: str,
account: Optional[Any] = None,
task: Optional[Any] = None,
context: Optional[Dict[str, Any]] = None
) -> Tuple[str, bool, str]:
"""
Get prompt for a function with metadata about source.
Priority:
1. task.prompt_override (if task provided and has override)
2. DB prompt for (account, function) - marked as custom if is_customized=True
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
Args:
function_name: AI function name (e.g., 'auto_cluster', 'generate_ideas')
account: Account object (optional)
task: Task object with optional prompt_override (optional)
context: Additional context for prompt rendering (optional)
Returns:
Tuple of (prompt_string, is_custom, prompt_type)
- prompt_string: The rendered prompt
- is_custom: True if using custom/account prompt, False if global
- prompt_type: The prompt type identifier
"""
# Step 1: Get prompt type
prompt_type = cls.FUNCTION_TO_PROMPT_TYPE.get(function_name, function_name)
# Step 2: Check task-level override (always considered custom)
if task and hasattr(task, 'prompt_override') and task.prompt_override:
logger.info(f"Using task-level prompt override for {function_name}")
prompt = task.prompt_override
return cls._render_prompt(prompt, context or {}), True, prompt_type
# Step 3: Try DB prompt (account-specific)
if account:
try:
from igny8_core.modules.system.models import AIPrompt
db_prompt = AIPrompt.objects.get(
account=account,
prompt_type=prompt_type,
is_active=True
)
# Check if prompt is customized
is_custom = db_prompt.is_customized
logger.info(f"Using {'customized' if is_custom else 'default'} account prompt for {function_name} (account {account.id})")
prompt = db_prompt.prompt_value
return cls._render_prompt(prompt, context or {}), is_custom, prompt_type
except Exception as e:
logger.debug(f"No account-specific prompt found for {function_name}: {e}")
# Step 4: Try GlobalAIPrompt (platform-wide default) - REQUIRED
try:
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=prompt_type,
is_active=True
)
logger.info(f"Using global default prompt for {function_name} from GlobalAIPrompt")
prompt = global_prompt.prompt_value
return cls._render_prompt(prompt, context or {}), False, prompt_type
except Exception as e:
error_msg = (
f"ERROR: Global prompt '{prompt_type}' not found for function '{function_name}'. "
f"Please configure it in Django admin at: /admin/system/globalaiprompt/. "
f"Error: {e}"
)
logger.error(error_msg)
raise ValueError(error_msg)
@classmethod
def get_prompt(
cls,
@@ -46,63 +153,23 @@ class PromptRegistry:
) -> str:
"""
Get prompt for a function with hierarchical resolution.
Priority:
1. task.prompt_override (if task provided and has override)
2. DB prompt for (account, function)
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
Args:
function_name: AI function name (e.g., 'auto_cluster', 'generate_ideas')
account: Account object (optional)
task: Task object with optional prompt_override (optional)
context: Additional context for prompt rendering (optional)
Returns:
Prompt string ready for formatting
"""
# Step 1: Check task-level override
if task and hasattr(task, 'prompt_override') and task.prompt_override:
logger.info(f"Using task-level prompt override for {function_name}")
prompt = task.prompt_override
return cls._render_prompt(prompt, context or {})
# Step 2: Get prompt type
prompt_type = cls.FUNCTION_TO_PROMPT_TYPE.get(function_name, function_name)
# Step 3: Try DB prompt (account-specific)
if account:
try:
from igny8_core.modules.system.models import AIPrompt
db_prompt = AIPrompt.objects.get(
account=account,
prompt_type=prompt_type,
is_active=True
)
logger.info(f"Using account-specific prompt for {function_name} (account {account.id})")
prompt = db_prompt.prompt_value
return cls._render_prompt(prompt, context or {})
except Exception as e:
logger.debug(f"No account-specific prompt found for {function_name}: {e}")
# Step 4: Try GlobalAIPrompt (platform-wide default) - REQUIRED
try:
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=prompt_type,
is_active=True
)
logger.info(f"Using global default prompt for {function_name} from GlobalAIPrompt")
prompt = global_prompt.prompt_value
return cls._render_prompt(prompt, context or {})
except Exception as e:
error_msg = (
f"ERROR: Global prompt '{prompt_type}' not found for function '{function_name}'. "
f"Please configure it in Django admin at: /admin/system/globalaiprompt/. "
f"Error: {e}"
)
logger.error(error_msg)
raise ValueError(error_msg)
prompt, _, _ = cls.get_prompt_with_metadata(function_name, account, task, context)
return prompt
@classmethod
def _render_prompt(cls, prompt_template: str, context: Dict[str, Any]) -> str:
@@ -219,3 +286,61 @@ def get_prompt(function_name: str, account=None, task=None, context=None) -> str
"""Get prompt using registry"""
return PromptRegistry.get_prompt(function_name, account=account, task=task, context=context)
def get_prompt_with_prefix(function_name: str, account=None, task=None, context=None) -> Tuple[str, str]:
"""
Get prompt with its tracking prefix.
Args:
function_name: AI function name
account: Account object (optional)
task: Task object with optional prompt_override (optional)
context: Additional context for prompt rendering (optional)
Returns:
Tuple of (prompt_string, prefix_string)
- prompt_string: The rendered prompt
- prefix_string: The tracking prefix (e.g., '##GP01-Clustering' or '##CP01-Clustering')
"""
prompt, is_custom, prompt_type = PromptRegistry.get_prompt_with_metadata(
function_name, account=account, task=task, context=context
)
prefix = PromptRegistry.get_prompt_prefix(prompt_type, is_custom)
return prompt, prefix
def get_prompt_prefix_for_function(function_name: str, account=None, task=None) -> str:
"""
Get just the prefix for a function without fetching the full prompt.
Useful when the prompt was already fetched elsewhere.
Args:
function_name: AI function name
account: Account object (optional)
task: Task object with optional prompt_override (optional)
Returns:
The tracking prefix (e.g., '##GP01-Clustering' or '##CP01-Clustering')
"""
prompt_type = PromptRegistry.FUNCTION_TO_PROMPT_TYPE.get(function_name, function_name)
# Check for task-level override (always custom)
if task and hasattr(task, 'prompt_override') and task.prompt_override:
return PromptRegistry.get_prompt_prefix(prompt_type, is_custom=True)
# Check for account-specific prompt
if account:
try:
from igny8_core.modules.system.models import AIPrompt
db_prompt = AIPrompt.objects.get(
account=account,
prompt_type=prompt_type,
is_active=True
)
return PromptRegistry.get_prompt_prefix(prompt_type, is_custom=db_prompt.is_customized)
except Exception:
pass
# Fallback to global (not custom)
return PromptRegistry.get_prompt_prefix(prompt_type, is_custom=False)