28 KiB
28 KiB
AI Master Architecture Document
Clustering, Idea Generation, and Content Generation
Version: 1.0
Date: 2025-01-XX
Scope: Complete architecture for 3 verified AI functions (clustering, idea generation, content generation)
Table of Contents
1. Common Architecture
1.1 Core Framework Files
Entry Point
- File:
backend/igny8_core/ai/tasks.py - Function:
run_ai_task - Purpose: Unified Celery task entrypoint for all AI functions
- Parameters:
function_name(str),payload(dict),account_id(int) - Flow: Loads function from registry → Creates AIEngine → Executes function
Engine Orchestrator
- File:
backend/igny8_core/ai/engine.py - Class:
AIEngine - Purpose: Central orchestrator managing lifecycle, progress, logging, cost tracking
- Methods:
execute- Main execution pipeline (6 phases: INIT, PREP, AI_CALL, PARSE, SAVE, DONE)_handle_error- Centralized error handling_log_to_database- Logs to AITaskLog model- Helper methods:
_get_input_description,_build_validation_message,_get_prep_message,_get_ai_call_message,_get_parse_message,_get_parse_message_with_count,_get_save_message,_calculate_credits_for_clustering
Base Function Class
- File:
backend/igny8_core/ai/base.py - Class:
BaseAIFunction - Purpose: Abstract base class defining interface for all AI functions
- Abstract Methods:
get_name- Returns function name (e.g., 'auto_cluster')prepare- Loads and prepares databuild_prompt- Builds AI promptparse_response- Parses AI responsesave_output- Saves results to database
- Optional Methods:
get_metadata- Returns display name, description, phasesget_max_items- Returns max items limit (or None)validate- Validates input payload (default: checks for 'ids')get_model- Returns model override (default: None, uses account default)
Function Registry
- File:
backend/igny8_core/ai/registry.py - Functions:
register_function- Registers function classregister_lazy_function- Registers lazy loaderget_function- Gets function class by name (lazy loads if needed)get_function_instance- Gets function instance by namelist_functions- Lists all registered functions
- Lazy Loaders:
_load_auto_cluster- Loads AutoClusterFunction_load_generate_ideas- Loads GenerateIdeasFunction_load_generate_content- Loads GenerateContentFunction
AI Core Handler
- File:
backend/igny8_core/ai/ai_core.py - Class:
AICore - Purpose: Centralized AI request handler for all text generation
- Methods:
run_ai_request- Makes API call to OpenAI/Runwareextract_json- Extracts JSON from response (handles markdown code blocks)
Prompt Registry
- File:
backend/igny8_core/ai/prompts.py - Class:
PromptRegistry - Purpose: Centralized prompt management with hierarchical resolution
- Method:
get_prompt- Gets prompt with resolution order:- Task-level prompt_override (if exists)
- DB prompt for (account, function)
- Default fallback from DEFAULT_PROMPTS registry
- Prompt Types:
clustering- For auto_cluster functionideas- For generate_ideas functioncontent_generation- For generate_content function
- Context Placeholders:
[IGNY8_KEYWORDS]- Replaced with keyword list[IGNY8_CLUSTERS]- Replaced with cluster list[IGNY8_CLUSTER_KEYWORDS]- Replaced with cluster keywords[IGNY8_IDEA]- Replaced with idea data[IGNY8_CLUSTER]- Replaced with cluster data[IGNY8_KEYWORDS]- Replaced with keywords (for content)
Model Settings
- File:
backend/igny8_core/ai/settings.py - Constants:
MODEL_CONFIG- Model configurations per function (model, max_tokens, temperature, response_format)FUNCTION_ALIASES- Legacy function name mappings
- Functions:
get_model_config- Gets model config for function (reads from IntegrationSettings if account provided)get_model- Gets model name for functionget_max_tokens- Gets max tokens for functionget_temperature- Gets temperature for function
Validators
- File:
backend/igny8_core/ai/validators.py - Functions:
validate_ids- Validates 'ids' array in payloadvalidate_keywords_exist- Validates keywords exist in databasevalidate_cluster_exists- Validates cluster existsvalidate_tasks_exist- Validates tasks existvalidate_cluster_limits- Validates plan limits (currently disabled - always returns valid)validate_api_key- Validates API key is configuredvalidate_model- Validates model is in supported listvalidate_image_size- Validates image size for model
Progress Tracking
- File:
backend/igny8_core/ai/tracker.py - Classes:
StepTracker- Tracks request/response stepsProgressTracker- Tracks Celery progress updatesCostTracker- Tracks API costs and tokensConsoleStepTracker- Console-based step logging
Database Logging
- File:
backend/igny8_core/ai/models.py - Model:
AITaskLog - Fields:
task_id,function_name,account,phase,message,status,duration,cost,tokens,request_steps,response_steps,error,payload,result
1.2 Execution Flow (All Functions)
1. API Endpoint (views.py)
↓
2. run_ai_task (tasks.py)
- Gets account from account_id
- Gets function instance from registry
- Creates AIEngine
↓
3. AIEngine.execute (engine.py)
Phase 1: INIT (0-10%)
- Calls function.validate()
- Updates progress tracker
↓
Phase 2: PREP (10-25%)
- Calls function.prepare()
- Calls function.build_prompt()
- Updates progress tracker
↓
Phase 3: AI_CALL (25-70%)
- Gets model config from settings
- Calls AICore.run_ai_request()
- Tracks cost and tokens
- Updates progress tracker
↓
Phase 4: PARSE (70-85%)
- Calls function.parse_response()
- Updates progress tracker
↓
Phase 5: SAVE (85-98%)
- Calls function.save_output()
- Logs credit usage
- Updates progress tracker
↓
Phase 6: DONE (98-100%)
- Logs to AITaskLog
- Returns result
2. Auto Cluster Keywords
2.1 Function Implementation
- File:
backend/igny8_core/ai/functions/auto_cluster.py - Class:
AutoClusterFunction - Inherits:
BaseAIFunction
2.2 API Endpoint
- File:
backend/igny8_core/modules/planner/views.py - ViewSet:
KeywordViewSet - Action:
auto_cluster - Method: POST
- URL Path:
/v1/planner/keywords/auto_cluster/ - Payload:
ids(list[int]) - Keyword IDs to clustersector_id(int, optional) - Sector ID for filtering
- Response:
success(bool)task_id(str) - Celery task ID if asyncclusters_created(int) - Number of clusters createdkeywords_updated(int) - Number of keywords updatedmessage(str)
2.3 Function Methods
get_name()
- Returns:
'auto_cluster'
get_metadata()
- Returns: Dict with
display_name,description,phases(INIT, PREP, AI_CALL, PARSE, SAVE, DONE)
get_max_items()
- Returns:
None(no limit)
validate(payload, account)
- Validates:
- Calls
validate_idsto check for 'ids' array - Calls
validate_keywords_existto verify keywords exist
- Calls
- Returns: Dict with
valid(bool) and optionalerror(str)
prepare(payload, account)
- Loads:
- Keywords from database (filters by
ids,account, optionalsector_id) - Uses
select_relatedfor:account,site,site__account,sector,sector__site
- Keywords from database (filters by
- Returns: Dict with:
keywords(list[Keyword objects])keyword_data(list[dict]) - Formatted data with:id,keyword,volume,difficulty,intentsector_id(int, optional)
build_prompt(data, account)
- Gets Prompt:
- Calls
PromptRegistry.get_prompt(function_name='auto_cluster', account, context) - Context includes:
KEYWORDS(formatted keyword list), optionalSECTOR(sector name)
- Calls
- Formatting:
- Formats keywords as:
"- {keyword} (Volume: {volume}, Difficulty: {difficulty}, Intent: {intent})" - Replaces
[IGNY8_KEYWORDS]placeholder - Adds JSON mode instruction if not present
- Formats keywords as:
- Returns: Prompt string
parse_response(response, step_tracker)
- Parsing:
- Tries direct JSON parse first
- Falls back to
AICore.extract_json()if needed (handles markdown code blocks)
- Extraction:
- Extracts
clustersarray from JSON - Handles both dict with 'clusters' key and direct array
- Extracts
- Returns: List[Dict] with cluster data:
name(str) - Cluster namedescription(str) - Cluster descriptionkeywords(list[str]) - List of keyword strings
save_output(parsed, original_data, account, progress_tracker, step_tracker)
- Input:
parsed- List of cluster dicts from parse_responseoriginal_data- Dict from prepare() withkeywordsandsector_id
- Process:
- Gets account, site, sector from first keyword
- For each cluster in parsed:
- Gets or creates
Clustersrecord:- Fields:
name,description,account,site,sector,status='active' - Uses
get_or_createwith name + account + site + sector
- Fields:
- Matches keywords (case-insensitive):
- Normalizes cluster keywords and available keywords to lowercase
- Updates matched
Keywordsrecords:- Sets
clusterforeign key - Sets
status='mapped'
- Sets
- Gets or creates
- Recalculates cluster metrics:
keywords_count- Count of keywords in clustervolume- Sum of keyword volumes (usesvolume_overrideif available, elseseed_keyword__volume)
- Returns: Dict with:
count(int) - Clusters createdclusters_created(int) - Clusters createdkeywords_updated(int) - Keywords updated
2.4 Database Models
Keywords Model
- File:
backend/igny8_core/modules/planner/models.py - Model:
Keywords - Fields Used:
id- Keyword IDseed_keyword(ForeignKey) - Reference to SeedKeywordkeyword(property) - Gets keyword text from seed_keywordvolume(property) - Gets volume from volume_override or seed_keyworddifficulty(property) - Gets difficulty from difficulty_override or seed_keywordintent(property) - Gets intent from seed_keywordcluster(ForeignKey) - Assigned clusterstatus- Status ('active', 'pending', 'mapped', 'archived')account,site,sector- From SiteSectorBaseModel
Clusters Model
- File:
backend/igny8_core/modules/planner/models.py - Model:
Clusters - Fields Used:
name- Cluster name (unique)description- Cluster descriptionkeywords_count- Count of keywords (recalculated)volume- Sum of keyword volumes (recalculated)status- Status ('active')account,site,sector- From SiteSectorBaseModel
2.5 AI Response Format
Expected JSON:
{
"clusters": [
{
"name": "Cluster Name",
"description": "Cluster description",
"keywords": ["keyword1", "keyword2", "keyword3"]
}
]
}
2.6 Progress Messages
- INIT: "Validating {keyword1}, {keyword2}, {keyword3} and {X} more keywords" (shows first 3, then count)
- PREP: "Loading {count} keyword(s)"
- AI_CALL: "Generating clusters with Igny8 Semantic SEO Model"
- PARSE: "{count} cluster(s) created"
- SAVE: "Saving {count} cluster(s)"
3. Generate Ideas
3.1 Function Implementation
- File:
backend/igny8_core/ai/functions/generate_ideas.py - Class:
GenerateIdeasFunction - Inherits:
BaseAIFunction
3.2 API Endpoint
- File:
backend/igny8_core/modules/planner/views.py - ViewSet:
ClusterViewSet - Action:
auto_generate_ideas - Method: POST
- URL Path:
/v1/planner/clusters/auto_generate_ideas/ - Payload:
ids(list[int]) - Cluster IDs (max 10)
- Response:
success(bool)task_id(str) - Celery task ID if asyncideas_created(int) - Number of ideas createdmessage(str)
3.3 Function Methods
get_name()
- Returns:
'generate_ideas'
get_metadata()
- Returns: Dict with
display_name,description,phases(INIT, PREP, AI_CALL, PARSE, SAVE, DONE)
get_max_items()
- Returns:
10(max clusters per generation)
validate(payload, account)
- Validates:
- Calls
super().validate()to check for 'ids' array and max_items limit - Calls
validate_cluster_existsfor first cluster ID - Calls
validate_cluster_limitsfor plan limits (currently disabled)
- Calls
- Returns: Dict with
valid(bool) and optionalerror(str)
prepare(payload, account)
- Loads:
- Clusters from database (filters by
ids,account) - Uses
select_relatedfor:sector,account,site,sector__site - Uses
prefetch_relatedfor:keywords
- Clusters from database (filters by
- Gets Keywords:
- For each cluster, loads
Keywordswithselect_related('seed_keyword') - Extracts keyword text from
seed_keyword.keyword
- For each cluster, loads
- Returns: Dict with:
clusters(list[Cluster objects])cluster_data(list[dict]) - Formatted data with:id,name,description,keywords(list[str])account(Account object)
build_prompt(data, account)
- Gets Prompt:
- Calls
PromptRegistry.get_prompt(function_name='generate_ideas', account, context) - Context includes:
CLUSTERS- Formatted cluster list:"Cluster ID: {id} | Name: {name} | Description: {description}"CLUSTER_KEYWORDS- Formatted cluster keywords:"Cluster ID: {id} | Name: {name} | Keywords: {keyword1}, {keyword2}"
- Calls
- Replaces Placeholders:
[IGNY8_CLUSTERS]→ clusters_text[IGNY8_CLUSTER_KEYWORDS]→ cluster_keywords_text
- Returns: Prompt string
parse_response(response, step_tracker)
- Parsing:
- Calls
AICore.extract_json()to extract JSON from response - Validates 'ideas' key exists in JSON
- Calls
- Returns: List[Dict] with idea data:
title(str) - Idea titledescription(str or dict) - Idea description (can be JSON string)content_type(str) - Content type ('blog_post', 'article', etc.)content_structure(str) - Content structure ('cluster_hub', 'supporting_page', etc.)cluster_id(int, optional) - Cluster ID referencecluster_name(str, optional) - Cluster name referenceestimated_word_count(int) - Estimated word countcovered_keywordsortarget_keywords(str) - Target keywords
save_output(parsed, original_data, account, progress_tracker, step_tracker)
- Input:
parsed- List of idea dicts from parse_responseoriginal_data- Dict from prepare() withclustersandcluster_data
- Process:
- For each idea in parsed:
- Matches cluster:
- First tries by
cluster_idfrom AI response - Falls back to
cluster_namematching - Last resort: position-based matching (first idea → first cluster)
- First tries by
- Gets site from cluster (or cluster.sector.site)
- Handles description:
- If dict, converts to JSON string
- If not string, converts to string
- Creates
ContentIdeasrecord:- Fields:
idea_title- Fromtitledescription- Processed descriptioncontent_type- Fromcontent_type(default: 'blog_post')content_structure- Fromcontent_structure(default: 'supporting_page')target_keywords- Fromcovered_keywordsortarget_keywordskeyword_cluster- Matched clusterestimated_word_count- Fromestimated_word_count(default: 1500)status- 'new'account,site,sector- From cluster
- Fields:
- Matches cluster:
- For each idea in parsed:
- Returns: Dict with:
count(int) - Ideas createdideas_created(int) - Ideas created
3.4 Database Models
Clusters Model
- File:
backend/igny8_core/modules/planner/models.py - Model:
Clusters - Fields Used:
id- Cluster IDname- Cluster namedescription- Cluster descriptionkeywords(related_name) - Related Keywordsaccount,site,sector- From SiteSectorBaseModel
ContentIdeas Model
- File:
backend/igny8_core/modules/planner/models.py - Model:
ContentIdeas - Fields Used:
idea_title- Idea titledescription- Idea description (can be JSON string)content_type- Content type ('blog_post', 'article', 'guide', 'tutorial')content_structure- Content structure ('cluster_hub', 'landing_page', 'pillar_page', 'supporting_page')target_keywords- Target keywords stringkeyword_cluster(ForeignKey) - Related clusterestimated_word_count- Estimated word countstatus- Status ('new', 'scheduled', 'published')account,site,sector- From SiteSectorBaseModel
3.5 AI Response Format
Expected JSON:
{
"ideas": [
{
"title": "Idea Title",
"description": "Idea description or JSON structure",
"content_type": "blog_post",
"content_structure": "supporting_page",
"cluster_id": 1,
"cluster_name": "Cluster Name",
"estimated_word_count": 1500,
"covered_keywords": "keyword1, keyword2"
}
]
}
3.6 Progress Messages
- INIT: "Verifying cluster integrity"
- PREP: "Loading cluster keywords"
- AI_CALL: "Generating ideas with Igny8 Semantic AI"
- PARSE: "{count} high-opportunity idea(s) generated"
- SAVE: "Content Outline for Ideas generated"
4. Generate Content
4.1 Function Implementation
- File:
backend/igny8_core/ai/functions/generate_content.py - Class:
GenerateContentFunction - Inherits:
BaseAIFunction
4.2 API Endpoint
- File:
backend/igny8_core/modules/writer/views.py - ViewSet:
TasksViewSet - Action:
auto_generate_content - Method: POST
- URL Path:
/v1/writer/tasks/auto_generate_content/ - Payload:
ids(list[int]) - Task IDs (max 10)
- Response:
success(bool)task_id(str) - Celery task ID if asynctasks_updated(int) - Number of tasks updatedmessage(str)
4.3 Function Methods
get_name()
- Returns:
'generate_content'
get_metadata()
- Returns: Dict with
display_name,description,phases(INIT, PREP, AI_CALL, PARSE, SAVE, DONE)
get_max_items()
- Returns:
50(max tasks per batch)
validate(payload, account)
- Validates:
- Calls
super().validate()to check for 'ids' array and max_items limit - Calls
validate_tasks_existto verify tasks exist
- Calls
- Returns: Dict with
valid(bool) and optionalerror(str)
prepare(payload, account)
- Loads:
- Tasks from database (filters by
ids,account) - Uses
select_relatedfor:account,site,sector,cluster,idea
- Tasks from database (filters by
- Returns: List[Task objects]
build_prompt(data, account)
- Input: Can be single Task or list[Task] (handles first task if list)
- Builds Idea Data:
title- From task.titledescription- From task.descriptionoutline- From task.idea.description (handles JSON structure):- If JSON, formats as:
"## {H2 heading}\n### {H3 subheading}\nContent Type: {type}\nDetails: {details}" - If plain text, uses as-is
- If JSON, formats as:
structure- From task.idea.content_structure or task.content_structuretype- From task.idea.content_type or task.content_typeestimated_word_count- From task.idea.estimated_word_count
- Builds Cluster Data:
cluster_name- From task.cluster.namedescription- From task.cluster.descriptionstatus- From task.cluster.status
- Builds Keywords Data:
- From task.keywords (legacy) or task.idea.target_keywords
- Gets Prompt:
- Calls
PromptRegistry.get_prompt(function_name='generate_content', account, task, context) - Context includes:
IDEA- Formatted idea data stringCLUSTER- Formatted cluster data stringKEYWORDS- Keywords string
- Calls
- Returns: Prompt string
parse_response(response, step_tracker)
- Parsing:
- First tries JSON parse:
- If successful and dict, returns dict
- Falls back to plain text:
- Calls
normalize_content()fromcontent_normalizerto convert to HTML - Returns dict with
contentfield
- Calls
- First tries JSON parse:
- Returns: Dict with:
- If JSON:
content(str) - HTML contenttitle(str, optional) - Content titlemeta_title(str, optional) - Meta titlemeta_description(str, optional) - Meta descriptionword_count(int, optional) - Word countprimary_keyword(str, optional) - Primary keywordsecondary_keywords(list, optional) - Secondary keywordstags(list, optional) - Tagscategories(list, optional) - Categories
- If Plain Text:
content(str) - Normalized HTML content
- If JSON:
save_output(parsed, original_data, account, progress_tracker, step_tracker)
- Input:
parsed- Dict from parse_responseoriginal_data- Task object or list[Task] (handles first task if list)
- Process:
- Extracts content fields from parsed dict:
content_html- Fromcontentfieldtitle- Fromtitleor task.titlemeta_title- Frommeta_titleor task.meta_title or task.titlemeta_description- Frommeta_descriptionor task.meta_description or task.descriptionword_count- Fromword_countor calculated from contentprimary_keyword- Fromprimary_keywordsecondary_keywords- Fromsecondary_keywords(converts to list if needed)tags- Fromtags(converts to list if needed)categories- Fromcategories(converts to list if needed)
- Calculates word count if not provided:
- Strips HTML tags and counts words
- Gets or creates
Contentrecord:- Uses
get_or_createwithtask(OneToOne relationship) - Defaults:
html_content,word_count,status='draft',account,site,sector
- Uses
- Updates
Contentfields:html_content- Content HTMLword_count- Word counttitle- Content titlemeta_title- Meta titlemeta_description- Meta descriptionprimary_keyword- Primary keywordsecondary_keywords- Secondary keywords (JSONField)tags- Tags (JSONField)categories- Categories (JSONField)status- Always 'draft' for newly generated contentmetadata- Extra fields from parsed dict (excludes standard fields)account,site,sector,task- Aligned from task
- Updates
Tasksrecord:- Sets
status='completed' - Updates
updated_at
- Sets
- Extracts content fields from parsed dict:
- Returns: Dict with:
count(int) - Tasks updated (always 1 per task)tasks_updated(int) - Tasks updatedword_count(int) - Word count
4.4 Database Models
Tasks Model
- File:
backend/igny8_core/modules/writer/models.py - Model:
Tasks - Fields Used:
id- Task IDtitle- Task titledescription- Task descriptionkeywords- Keywords string (legacy)cluster(ForeignKey) - Related clusteridea(ForeignKey) - Related ContentIdeascontent_structure- Content structurecontent_type- Content typestatus- Status ('queued', 'completed')meta_title- Meta titlemeta_description- Meta descriptionaccount,site,sector- From SiteSectorBaseModel
Content Model
- File:
backend/igny8_core/modules/writer/models.py - Model:
Content - Fields Used:
task(OneToOneField) - Related taskhtml_content- HTML contentword_count- Word counttitle- Content titlemeta_title- Meta titlemeta_description- Meta descriptionprimary_keyword- Primary keywordsecondary_keywords(JSONField) - Secondary keywords listtags(JSONField) - Tags listcategories(JSONField) - Categories liststatus- Status ('draft', 'review', 'published')metadata(JSONField) - Additional metadataaccount,site,sector- From SiteSectorBaseModel (auto-set from task)
4.5 AI Response Format
Expected JSON:
{
"content": "<html>Content HTML</html>",
"title": "Content Title",
"meta_title": "Meta Title",
"meta_description": "Meta description",
"word_count": 1500,
"primary_keyword": "primary keyword",
"secondary_keywords": ["keyword1", "keyword2"],
"tags": ["tag1", "tag2"],
"categories": ["category1"]
}
Or Plain Text:
Plain text content that will be normalized to HTML
4.6 Progress Messages
- INIT: "Validating task"
- PREP: "Preparing content idea"
- AI_CALL: "Writing article with Igny8 Semantic AI"
- PARSE: "{count} article(s) created"
- SAVE: "Saving article"
5. Change Guide
5.1 Where to Change Validation Logic
- File:
backend/igny8_core/ai/validators.py - Functions:
validate_ids,validate_keywords_exist,validate_cluster_exists,validate_tasks_exist - Or: Override
validate()method in function class
5.2 Where to Change Data Loading
- File: Function-specific file (e.g.,
auto_cluster.py) - Method:
prepare() - Change: Modify queryset filters, select_related, prefetch_related
5.3 Where to Change Prompts
- File:
backend/igny8_core/ai/prompts.py - Method:
PromptRegistry.get_prompt() - Change: Modify
DEFAULT_PROMPTSdict or update database prompts
5.4 Where to Change Model Configuration
- File:
backend/igny8_core/ai/settings.py - Constant:
MODEL_CONFIG - Change: Update model, max_tokens, temperature, response_format per function
5.5 Where to Change Response Parsing
- File: Function-specific file (e.g.,
generate_content.py) - Method:
parse_response() - Change: Modify JSON extraction or plain text handling
5.6 Where to Change Database Saving
- File: Function-specific file (e.g.,
auto_cluster.py) - Method:
save_output() - Change: Modify model creation/update logic, field mappings
5.7 Where to Change Progress Messages
- File:
backend/igny8_core/ai/engine.py - Methods:
_get_prep_message(),_get_ai_call_message(),_get_parse_message(),_get_save_message() - Or: Override in function class
get_metadata()phases
5.8 Where to Change Error Handling
- File:
backend/igny8_core/ai/engine.py - Method:
_handle_error() - Change: Modify error logging, error response format
6. Dependencies
6.1 Function Dependencies
- All functions depend on:
BaseAIFunction,AICore,PromptRegistry,get_model_config - Clustering depends on:
Keywords,Clustersmodels - Ideas depends on:
Clusters,ContentIdeas,Keywordsmodels - Content depends on:
Tasks,Content,ContentIdeas,Clustersmodels
6.2 External Dependencies
- Celery: For async task execution (
run_ai_task) - OpenAI API: For AI text generation (via
AICore.run_ai_request) - Django ORM: For database operations
- IntegrationSettings: For account-specific model configuration
7. Key Relationships
7.1 Clustering Flow
Keywords → Clusters (many-to-one)
- Keywords.cluster (ForeignKey)
- Clusters.keywords (related_name)
7.2 Ideas Flow
Clusters → ContentIdeas (one-to-many)
- ContentIdeas.keyword_cluster (ForeignKey)
- Clusters.ideas (related_name, if exists)
7.3 Content Flow
Tasks → Content (one-to-one)
- Content.task (OneToOneField)
- Tasks.content_record (related_name)
Tasks → ContentIdeas (many-to-one)
- Tasks.idea (ForeignKey)
- ContentIdeas.tasks (related_name)
Tasks → Clusters (many-to-one)
- Tasks.cluster (ForeignKey)
- Clusters.tasks (related_name)
8. Notes
- All functions use the same execution pipeline through
AIEngine.execute() - Progress tracking is handled automatically by
AIEngine - Cost tracking is handled automatically by
CostTracker - Database logging is handled automatically by
AITaskLog - Model configuration can be overridden per account via
IntegrationSettings - Prompts can be overridden per account via database prompts
- All functions support both async (Celery) and sync execution
- Error handling is centralized in
AIEngine._handle_error()