# 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-common-architecture) 2. [Auto Cluster Keywords](#2-auto-cluster-keywords) 3. [Generate Ideas](#3-generate-ideas) 4. [Generate Content](#4-generate-content) --- ## 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 data - `build_prompt` - Builds AI prompt - `parse_response` - Parses AI response - `save_output` - Saves results to database - **Optional Methods:** - `get_metadata` - Returns display name, description, phases - `get_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 class - `register_lazy_function` - Registers lazy loader - `get_function` - Gets function class by name (lazy loads if needed) - `get_function_instance` - Gets function instance by name - `list_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/Runware - `extract_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: 1. Task-level prompt_override (if exists) 2. DB prompt for (account, function) 3. Default fallback from DEFAULT_PROMPTS registry - **Prompt Types:** - `clustering` - For auto_cluster function - `ideas` - For generate_ideas function - `content_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 function - `get_max_tokens` - Gets max tokens for function - `get_temperature` - Gets temperature for function #### Validators - **File:** `backend/igny8_core/ai/validators.py` - **Functions:** - `validate_ids` - Validates 'ids' array in payload - `validate_keywords_exist` - Validates keywords exist in database - `validate_cluster_exists` - Validates cluster exists - `validate_tasks_exist` - Validates tasks exist - `validate_cluster_limits` - Validates plan limits (currently disabled - always returns valid) - `validate_api_key` - Validates API key is configured - `validate_model` - Validates model is in supported list - `validate_image_size` - Validates image size for model #### Progress Tracking - **File:** `backend/igny8_core/ai/tracker.py` - **Classes:** - `StepTracker` - Tracks request/response steps - `ProgressTracker` - Tracks Celery progress updates - `CostTracker` - Tracks API costs and tokens - `ConsoleStepTracker` - 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 cluster - `sector_id` (int, optional) - Sector ID for filtering - **Response:** - `success` (bool) - `task_id` (str) - Celery task ID if async - `clusters_created` (int) - Number of clusters created - `keywords_updated` (int) - Number of keywords updated - `message` (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_ids` to check for 'ids' array - Calls `validate_keywords_exist` to verify keywords exist - **Returns:** Dict with `valid` (bool) and optional `error` (str) #### `prepare(payload, account)` - **Loads:** - Keywords from database (filters by `ids`, `account`, optional `sector_id`) - Uses `select_related` for: `account`, `site`, `site__account`, `sector`, `sector__site` - **Returns:** Dict with: - `keywords` (list[Keyword objects]) - `keyword_data` (list[dict]) - Formatted data with: `id`, `keyword`, `volume`, `difficulty`, `intent` - `sector_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), optional `SECTOR` (sector name) - **Formatting:** - Formats keywords as: `"- {keyword} (Volume: {volume}, Difficulty: {difficulty}, Intent: {intent})"` - Replaces `[IGNY8_KEYWORDS]` placeholder - Adds JSON mode instruction if not present - **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 `clusters` array from JSON - Handles both dict with 'clusters' key and direct array - **Returns:** List[Dict] with cluster data: - `name` (str) - Cluster name - `description` (str) - Cluster description - `keywords` (list[str]) - List of keyword strings #### `save_output(parsed, original_data, account, progress_tracker, step_tracker)` - **Input:** - `parsed` - List of cluster dicts from parse_response - `original_data` - Dict from prepare() with `keywords` and `sector_id` - **Process:** - Gets account, site, sector from first keyword - For each cluster in parsed: - Gets or creates `Clusters` record: - Fields: `name`, `description`, `account`, `site`, `sector`, `status='active'` - Uses `get_or_create` with name + account + site + sector - Matches keywords (case-insensitive): - Normalizes cluster keywords and available keywords to lowercase - Updates matched `Keywords` records: - Sets `cluster` foreign key - Sets `status='mapped'` - Recalculates cluster metrics: - `keywords_count` - Count of keywords in cluster - `volume` - Sum of keyword volumes (uses `volume_override` if available, else `seed_keyword__volume`) - **Returns:** Dict with: - `count` (int) - Clusters created - `clusters_created` (int) - Clusters created - `keywords_updated` (int) - Keywords updated ### 2.4 Database Models #### Keywords Model - **File:** `backend/igny8_core/modules/planner/models.py` - **Model:** `Keywords` - **Fields Used:** - `id` - Keyword ID - `seed_keyword` (ForeignKey) - Reference to SeedKeyword - `keyword` (property) - Gets keyword text from seed_keyword - `volume` (property) - Gets volume from volume_override or seed_keyword - `difficulty` (property) - Gets difficulty from difficulty_override or seed_keyword - `intent` (property) - Gets intent from seed_keyword - `cluster` (ForeignKey) - Assigned cluster - `status` - 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 description - `keywords_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:** ```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 async - `ideas_created` (int) - Number of ideas created - `message` (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_exists` for first cluster ID - Calls `validate_cluster_limits` for plan limits (currently disabled) - **Returns:** Dict with `valid` (bool) and optional `error` (str) #### `prepare(payload, account)` - **Loads:** - Clusters from database (filters by `ids`, `account`) - Uses `select_related` for: `sector`, `account`, `site`, `sector__site` - Uses `prefetch_related` for: `keywords` - **Gets Keywords:** - For each cluster, loads `Keywords` with `select_related('seed_keyword')` - Extracts keyword text from `seed_keyword.keyword` - **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}"` - **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 - **Returns:** List[Dict] with idea data: - `title` (str) - Idea title - `description` (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 reference - `cluster_name` (str, optional) - Cluster name reference - `estimated_word_count` (int) - Estimated word count - `covered_keywords` or `target_keywords` (str) - Target keywords #### `save_output(parsed, original_data, account, progress_tracker, step_tracker)` - **Input:** - `parsed` - List of idea dicts from parse_response - `original_data` - Dict from prepare() with `clusters` and `cluster_data` - **Process:** - For each idea in parsed: - Matches cluster: - First tries by `cluster_id` from AI response - Falls back to `cluster_name` matching - Last resort: position-based matching (first idea → first cluster) - Gets site from cluster (or cluster.sector.site) - Handles description: - If dict, converts to JSON string - If not string, converts to string - Creates `ContentIdeas` record: - Fields: - `idea_title` - From `title` - `description` - Processed description - `content_type` - From `content_type` (default: 'blog_post') - `content_structure` - From `content_structure` (default: 'supporting_page') - `target_keywords` - From `covered_keywords` or `target_keywords` - `keyword_cluster` - Matched cluster - `estimated_word_count` - From `estimated_word_count` (default: 1500) - `status` - 'new' - `account`, `site`, `sector` - From cluster - **Returns:** Dict with: - `count` (int) - Ideas created - `ideas_created` (int) - Ideas created ### 3.4 Database Models #### Clusters Model - **File:** `backend/igny8_core/modules/planner/models.py` - **Model:** `Clusters` - **Fields Used:** - `id` - Cluster ID - `name` - Cluster name - `description` - Cluster description - `keywords` (related_name) - Related Keywords - `account`, `site`, `sector` - From SiteSectorBaseModel #### ContentIdeas Model - **File:** `backend/igny8_core/modules/planner/models.py` - **Model:** `ContentIdeas` - **Fields Used:** - `idea_title` - Idea title - `description` - 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 string - `keyword_cluster` (ForeignKey) - Related cluster - `estimated_word_count` - Estimated word count - `status` - Status ('new', 'scheduled', 'published') - `account`, `site`, `sector` - From SiteSectorBaseModel ### 3.5 AI Response Format **Expected JSON:** ```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 async - `tasks_updated` (int) - Number of tasks updated - `message` (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_exist` to verify tasks exist - **Returns:** Dict with `valid` (bool) and optional `error` (str) #### `prepare(payload, account)` - **Loads:** - Tasks from database (filters by `ids`, `account`) - Uses `select_related` for: `account`, `site`, `sector`, `cluster`, `idea` - **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.title - `description` - From task.description - `outline` - 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 - `structure` - From task.idea.content_structure or task.content_structure - `type` - From task.idea.content_type or task.content_type - `estimated_word_count` - From task.idea.estimated_word_count - **Builds Cluster Data:** - `cluster_name` - From task.cluster.name - `description` - From task.cluster.description - `status` - 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 string - `CLUSTER` - Formatted cluster data string - `KEYWORDS` - Keywords string - **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()` from `content_normalizer` to convert to HTML - Returns dict with `content` field - **Returns:** Dict with: - **If JSON:** - `content` (str) - HTML content - `title` (str, optional) - Content title - `meta_title` (str, optional) - Meta title - `meta_description` (str, optional) - Meta description - `word_count` (int, optional) - Word count - `primary_keyword` (str, optional) - Primary keyword - `secondary_keywords` (list, optional) - Secondary keywords - `tags` (list, optional) - Tags - `categories` (list, optional) - Categories - **If Plain Text:** - `content` (str) - Normalized HTML content #### `save_output(parsed, original_data, account, progress_tracker, step_tracker)` - **Input:** - `parsed` - Dict from parse_response - `original_data` - Task object or list[Task] (handles first task if list) - **Process:** - Extracts content fields from parsed dict: - `content_html` - From `content` field - `title` - From `title` or task.title - `meta_title` - From `meta_title` or task.meta_title or task.title - `meta_description` - From `meta_description` or task.meta_description or task.description - `word_count` - From `word_count` or calculated from content - `primary_keyword` - From `primary_keyword` - `secondary_keywords` - From `secondary_keywords` (converts to list if needed) - `tags` - From `tags` (converts to list if needed) - `categories` - From `categories` (converts to list if needed) - Calculates word count if not provided: - Strips HTML tags and counts words - Gets or creates `Content` record: - Uses `get_or_create` with `task` (OneToOne relationship) - Defaults: `html_content`, `word_count`, `status='draft'`, `account`, `site`, `sector` - Updates `Content` fields: - `html_content` - Content HTML - `word_count` - Word count - `title` - Content title - `meta_title` - Meta title - `meta_description` - Meta description - `primary_keyword` - Primary keyword - `secondary_keywords` - Secondary keywords (JSONField) - `tags` - Tags (JSONField) - `categories` - Categories (JSONField) - `status` - Always 'draft' for newly generated content - `metadata` - Extra fields from parsed dict (excludes standard fields) - `account`, `site`, `sector`, `task` - Aligned from task - Updates `Tasks` record: - Sets `status='completed'` - Updates `updated_at` - **Returns:** Dict with: - `count` (int) - Tasks updated (always 1 per task) - `tasks_updated` (int) - Tasks updated - `word_count` (int) - Word count ### 4.4 Database Models #### Tasks Model - **File:** `backend/igny8_core/modules/writer/models.py` - **Model:** `Tasks` - **Fields Used:** - `id` - Task ID - `title` - Task title - `description` - Task description - `keywords` - Keywords string (legacy) - `cluster` (ForeignKey) - Related cluster - `idea` (ForeignKey) - Related ContentIdeas - `content_structure` - Content structure - `content_type` - Content type - `status` - Status ('queued', 'completed') - `meta_title` - Meta title - `meta_description` - Meta description - `account`, `site`, `sector` - From SiteSectorBaseModel #### Content Model - **File:** `backend/igny8_core/modules/writer/models.py` - **Model:** `Content` - **Fields Used:** - `task` (OneToOneField) - Related task - `html_content` - HTML content - `word_count` - Word count - `title` - Content title - `meta_title` - Meta title - `meta_description` - Meta description - `primary_keyword` - Primary keyword - `secondary_keywords` (JSONField) - Secondary keywords list - `tags` (JSONField) - Tags list - `categories` (JSONField) - Categories list - `status` - Status ('draft', 'review', 'published') - `metadata` (JSONField) - Additional metadata - `account`, `site`, `sector` - From SiteSectorBaseModel (auto-set from task) ### 4.5 AI Response Format **Expected JSON:** ```json { "content": "Content 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_PROMPTS` dict 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`, `Clusters` models - Ideas depends on: `Clusters`, `ContentIdeas`, `Keywords` models - Content depends on: `Tasks`, `Content`, `ContentIdeas`, `Clusters` models ### 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()`