This commit is contained in:
IGNY8 VPS (Salman)
2025-12-20 19:49:57 +00:00
parent 3283a83b42
commit 9e8ff4fbb1
18 changed files with 797 additions and 828 deletions

View File

@@ -0,0 +1,223 @@
# Integration Settings Workflow & Data Flow
## Part 1: How Global Settings Load on Frontend
### Admin Configures Global Settings
**URL**: `https://api.igny8.com/admin/system/globalintegrationsettings/1/change/`
**What's Stored**:
- Platform-wide API keys (OpenAI, DALL-E, Runware)
- Default model selections (gpt-4o-mini, dall-e-3, runware:97@1)
- Default parameters (temperature: 0.7, max_tokens: 8192)
- Default image settings (size, quality, style)
**Who Can Access**: Only platform administrators
### Normal User Opens Integration Page
**URL**: `https://app.igny8.com/settings/integration`
**What Happens**:
1. **Frontend Request**:
- User browser requests: `GET /api/v1/system/settings/integrations/openai/`
- User browser requests: `GET /api/v1/system/settings/integrations/image_generation/`
2. **Backend Processing** (`integration_views.py` - `get_settings()` method):
- Checks if user's account has custom overrides in `IntegrationSettings` table
- Gets global defaults from `GlobalIntegrationSettings` singleton
- Merges data with this priority:
- If account has overrides → use account settings
- If no overrides → use global defaults
- **NEVER returns API keys** (security)
3. **Response to Frontend**:
```
{
"id": "openai",
"enabled": true,
"model": "gpt-4o-mini", // From global OR account override
"temperature": 0.7, // From global OR account override
"max_tokens": 8192, // From global OR account override
"using_global": true // Flag: true if using defaults
}
```
4. **Frontend Display**:
- Shows current model selection
- Shows "Using platform defaults" badge if `using_global: true`
- Shows "Custom settings" badge if `using_global: false`
- User can change model, temperature, etc.
- **API key status is NOT shown** (user cannot see/change platform keys)
---
## Part 2: How User Changes Are Saved
### User Changes Settings on Frontend
1. **User Actions**:
- Opens settings modal
- Changes model from `gpt-4o-mini` to `gpt-4o`
- Changes temperature from `0.7` to `0.8`
- Clicks "Save"
2. **Frontend Request**:
- Sends: `PUT /api/v1/system/settings/integrations/openai/`
- Body: `{"model": "gpt-4o", "temperature": 0.8, "max_tokens": 8192}`
3. **Backend Processing** (`integration_views.py` - `save_settings()` method):
- **CRITICAL SECURITY**: Strips ANY API keys from request (apiKey, api_key, openai_api_key, etc.)
- Validates account exists
- Builds clean config with ONLY allowed overrides:
- For OpenAI: model, temperature, max_tokens
- For Image: service, model, image_quality, image_style, sizes
- Saves to `IntegrationSettings` table:
```
account_id: 123
integration_type: "openai"
config: {"model": "gpt-4o", "temperature": 0.8, "max_tokens": 8192}
is_active: true
```
4. **Database Structure**:
- **GlobalIntegrationSettings** (1 row, pk=1):
- Contains: API keys + default settings
- Used by: ALL accounts for API keys
- **IntegrationSettings** (multiple rows):
- Row per account per integration type
- Contains: ONLY overrides (no API keys)
- Example:
```
id | account_id | integration_type | config
100 | 123 | openai | {"model": "gpt-4o", "temperature": 0.8}
101 | 456 | openai | {"model": "gpt-4.1", "max_tokens": 4000}
102 | 123 | image_generation| {"service": "runware", "model": "runware:100@1"}
```
5. **Next Request from User**:
- Frontend requests: `GET /api/v1/system/settings/integrations/openai/`
- Backend finds IntegrationSettings row for account 123
- Returns: `{"model": "gpt-4o", "temperature": 0.8, "using_global": false}`
- User sees their custom settings
---
## Data Flow Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ ADMIN SIDE │
│ https://api.igny8.com/admin/ │
│ │
│ GlobalIntegrationSettings (pk=1) │
│ ├── openai_api_key: "sk-xxx" ← Platform-wide │
│ ├── openai_model: "gpt-4o-mini" ← Default │
│ ├── openai_temperature: 0.7 ← Default │
│ ├── dalle_api_key: "sk-xxx" ← Platform-wide │
│ ├── runware_api_key: "xxx" ← Platform-wide │
│ └── image_quality: "standard" ← Default │
└─────────────────────────────────────────────────────────────┘
│ Backend reads from
┌─────────────────────────────────────────────────────────────┐
│ BACKEND API LAYER │
│ integration_views.py │
│ │
│ get_settings(): │
│ 1. Load GlobalIntegrationSettings (for defaults) │
│ 2. Check IntegrationSettings (for account overrides) │
│ 3. Merge: account overrides > global defaults │
│ 4. Return to frontend (NO API keys) │
│ │
│ save_settings(): │
│ 1. Receive request from frontend │
│ 2. Strip ALL API keys (security) │
│ 3. Save ONLY overrides to IntegrationSettings │
└─────────────────────────────────────────────────────────────┘
│ API sends data
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND - USER SIDE │
│ https://app.igny8.com/settings/integration │
│ │
│ User sees: │
│ ├── Model: gpt-4o-mini (dropdown) │
│ ├── Temperature: 0.7 (slider) │
│ ├── Status: ✓ Connected (test connection works) │
│ └── Badge: "Using platform defaults" │
│ │
│ User CANNOT see: │
│ ✗ API keys (security) │
│ ✗ Platform configuration │
└─────────────────────────────────────────────────────────────┘
│ User changes settings
┌─────────────────────────────────────────────────────────────┐
│ IntegrationSettings Table │
│ (Per-account overrides - NO API KEYS) │
│ │
│ Account 123: │
│ ├── openai: {"model": "gpt-4o", "temperature": 0.8} │
│ └── image_generation: {"service": "runware"} │
│ │
│ Account 456: │
│ ├── openai: {"model": "gpt-4.1"} │
│ └── image_generation: (no row = uses global defaults) │
└─────────────────────────────────────────────────────────────┘
```
---
## Important Security Rules
1. **API Keys Flow**:
- Admin sets → GlobalIntegrationSettings
- Backend uses → For ALL accounts
- Frontend NEVER sees → Security
- Users NEVER save → Stripped by backend
2. **Settings Flow**:
- Admin sets defaults → GlobalIntegrationSettings
- Users customize → IntegrationSettings (overrides only)
- Backend merges → Global defaults + account overrides
- Frontend displays → Merged result (no keys)
3. **Free Plan Restriction**:
- Cannot create IntegrationSettings rows
- Must use global defaults only
- Enforced at frontend (UI disabled)
- TODO: Add backend validation
---
## Example Scenarios
### Scenario 1: New User First Visit
- User has NO IntegrationSettings row
- Backend returns global defaults
- `using_global: true`
- User sees platform defaults
- API operations use platform API key
### Scenario 2: User Customizes Model
- User changes model to "gpt-4o"
- Frontend sends: `{"model": "gpt-4o"}`
- Backend creates IntegrationSettings row
- Next visit: `using_global: false`
- API operations use platform API key + user's model choice
### Scenario 3: User Resets to Default
- Frontend sends: `{"model": "gpt-4o-mini"}` (same as global)
- Backend still saves override row
- Alternative: Delete row to truly use global
- TODO: Add "Reset to defaults" button
### Scenario 4: Admin Changes Global Default
- Admin changes global model to "gpt-4.1"
- Users WITH overrides: See their custom model
- Users WITHOUT overrides: See new "gpt-4.1" default
- All users: Use platform API key

View File

@@ -157,6 +157,14 @@ class Igny8AdminSite(UnfoldAdminSite):
('igny8_core_auth', 'SeedKeyword'),
],
},
'Global Settings': {
'models': [
('system', 'GlobalIntegrationSettings'),
('system', 'GlobalAIPrompt'),
('system', 'GlobalAuthorProfile'),
('system', 'GlobalStrategy'),
],
},
'Plans and Billing': {
'models': [
('igny8_core_auth', 'Plan'),

View File

@@ -1,6 +1,6 @@
"""
Prompt Registry - Centralized prompt management with override hierarchy
Supports: task-level overrides → DB prompts → default fallbacks
Supports: task-level overrides → DB prompts → GlobalAIPrompt (REQUIRED)
"""
import logging
from typing import Dict, Any, Optional
@@ -14,583 +14,11 @@ class PromptRegistry:
Centralized prompt registry with hierarchical resolution:
1. Task-level prompt_override (if exists)
2. DB prompt for (account, function)
3. Default fallback from registry
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
"""
# Default prompts stored in registry
DEFAULT_PROMPTS = {
'clustering': """You are a semantic strategist and SEO architecture engine. Your task is to analyze the provided keyword list and group them into meaningful, intent-driven topic clusters that reflect how real users search, think, and act online.
Return a single JSON object with a "clusters" array. Each cluster must follow this structure:
{
"name": "[Descriptive cluster name — natural, SEO-relevant, clearly expressing the topic]",
"description": "[12 concise sentences explaining what this cluster covers and why these keywords belong together]",
"keywords": ["keyword 1", "keyword 2", "keyword 3", "..."]
}
CLUSTERING STRATEGY:
1. Keyword-first, structure-follows:
- Do NOT rely on assumed categories or existing content structures.
- Begin purely from the meaning, intent, and behavioral connection between keywords.
2. Use multi-dimensional grouping logic:
- Group keywords by these behavioral dimensions:
• Search Intent → informational, commercial, transactional, navigational
• Use-Case or Problem → what the user is trying to achieve or solve
• Function or Feature → how something works or what it does
• Persona or Audience → who the content or product serves
• Context → location, time, season, platform, or device
- Combine 23 dimensions naturally where they make sense.
3. Model real search behavior:
- Favor clusters that form natural user journeys such as:
• Problem ➝ Solution
• General ➝ Specific
• Product ➝ Use-case
• Buyer ➝ Benefit
• Tool ➝ Function
• Task ➝ Method
- Each cluster should feel like a real topic hub users would explore in depth.
4. Avoid superficial groupings:
- Do not cluster keywords just because they share words.
- Do not force-fit outliers or unrelated keywords.
- Exclude keywords that don't logically connect to any cluster.
5. Quality rules:
- Each cluster should include between 310 strongly related keywords.
- Never duplicate a keyword across multiple clusters.
- Prioritize semantic strength, search intent, and usefulness for SEO-driven content structure.
- It's better to output fewer, high-quality clusters than many weak or shallow ones.
INPUT FORMAT:
{
"keywords": [IGNY8_KEYWORDS]
}
OUTPUT FORMAT:
Return ONLY the final JSON object in this format:
{
"clusters": [
{
"name": "...",
"description": "...",
"keywords": ["...", "...", "..."]
}
]
}
Do not include any explanations, text, or commentary outside the JSON output.
""",
'ideas': """Generate SEO-optimized, high-quality content ideas and outlines for each keyword cluster.
Input:
Clusters: [IGNY8_CLUSTERS]
Keywords: [IGNY8_CLUSTER_KEYWORDS]
Output: JSON with "ideas" array.
Each cluster → 1 cluster_hub + 24 supporting ideas.
Each idea must include:
title, description, content_type, content_structure, cluster_id, estimated_word_count (15002200), and covered_keywords.
Outline Rules:
Intro: 1 hook (3040 words) + 2 intro paragraphs (5060 words each).
58 H2 sections, each with 23 H3s.
Each H2 ≈ 250300 words, mixed content (paragraphs, lists, tables, blockquotes).
Vary section format and tone; no bullets or lists at start.
Tables have columns; blockquotes = expert POV or data insight.
Use depth, examples, and real context.
Avoid repetitive structure.
Tone: Professional editorial flow. No generic phrasing. Use varied sentence openings and realistic examples.
Output JSON Example:
{
"ideas": [
{
"title": "Best Organic Cotton Duvet Covers for All Seasons",
"description": {
"introduction": {
"hook": "Transform your sleep with organic cotton that blends comfort and sustainability.",
"paragraphs": [
{"format": "paragraph", "details": "Overview of organic cotton's rise in bedding industry."},
{"format": "paragraph", "details": "Why consumers prefer organic bedding over synthetic alternatives."}
]
},
"H2": [
{
"heading": "Why Choose Organic Cotton for Bedding?",
"subsections": [
{"subheading": "Health and Skin Benefits", "format": "paragraph", "details": "Discuss hypoallergenic and chemical-free aspects."},
{"subheading": "Environmental Sustainability", "format": "list", "details": "Eco benefits like low water use, no pesticides."},
{"subheading": "Long-Term Cost Savings", "format": "table", "details": "Compare durability and pricing over time."}
]
}
]
},
"content_type": "post",
"content_structure": "review",
"cluster_id": 12,
"estimated_word_count": 1800,
"covered_keywords": "organic duvet covers, eco-friendly bedding, sustainable sheets"
}
]
}
Valid content_type values: post, page, product, taxonomy
Valid content_structure by type:
- post: article, guide, comparison, review, listicle
- page: landing_page, business_page, service_page, general, cluster_hub
- product: product_page
- taxonomy: category_archive, tag_archive, attribute_archive""",
'content_generation': """You are an editorial content strategist. Your task is to generate a complete JSON response object based on the provided content idea, keyword cluster, keyword list, and metadata context.
==================
Generate a complete JSON response object matching this structure:
==================
{
"title": "[Article title using target keywords — full sentence case]",
"content": "[HTML content — full editorial structure with <p>, <h2>, <h3>, <ul>, <ol>, <table>]"
}
===========================
CONTENT FLOW RULES
===========================
**INTRODUCTION:**
- Start with 1 italicized hook (3040 words)
- Follow with 2 narrative paragraphs (each 5060 words; 23 sentences max)
- No headings allowed in intro
**H2 SECTIONS (58 total):**
Each section should be 250300 words and follow this format:
1. Two narrative paragraphs (80120 words each, 23 sentences)
2. One list or table (must come *after* a paragraph)
3. Optional closing paragraph (4060 words)
4. Insert 23 subsections naturally after main paragraphs
**Formatting Rules:**
- Vary use of unordered lists, ordered lists, and tables across sections
- Never begin any section or sub-section with a list or table
===========================
STYLE & QUALITY RULES
===========================
- **Keyword Usage:**
- Use keywords naturally in title, introduction, and headings
- Prioritize readability over keyword density
- **Tone & style guidelines:**
- No robotic or passive voice
- Avoid generic intros like "In today's world…"
- Don't repeat heading in opening sentence
- Vary sentence structure and length
===========================
STAGE 3: METADATA CONTEXT (NEW)
===========================
**Content Structure:**
[IGNY8_CONTENT_STRUCTURE]
- If structure is "cluster_hub": Create comprehensive, authoritative content that serves as the main resource for this topic cluster. Include overview sections, key concepts, and links to related topics.
- If structure is "article" or "guide": Create detailed, focused content that dives deep into the topic with actionable insights.
- Other structures: Follow the appropriate format (listicle, comparison, review, landing_page, service_page, product_page, category_archive, tag_archive, attribute_archive).
**Taxonomy Context:**
[IGNY8_TAXONOMY]
- Use taxonomy information to structure categories and tags appropriately.
- Align content with taxonomy hierarchy and relationships.
- Ensure content fits within the defined taxonomy structure.
**Product/Service Attributes:**
[IGNY8_ATTRIBUTES]
- If attributes are provided (e.g., product specs, service modifiers), incorporate them naturally into the content.
- For product content: Include specifications, features, dimensions, materials, etc. as relevant.
- For service content: Include service tiers, pricing modifiers, availability, etc. as relevant.
- Present attributes in a user-friendly format (tables, lists, or integrated into narrative).
===========================
INPUT VARIABLES
===========================
CONTENT IDEA DETAILS:
[IGNY8_IDEA]
KEYWORD CLUSTER:
[IGNY8_CLUSTER]
ASSOCIATED KEYWORDS:
[IGNY8_KEYWORDS]
===========================
OUTPUT FORMAT
===========================
Return ONLY the final JSON object.
Do NOT include any comments, formatting, or explanations.""",
'site_structure_generation': """You are a senior UX architect and information designer. Use the business brief, objectives, style references, and existing site info to propose a complete multi-page marketing website structure.
INPUT CONTEXT
==============
BUSINESS BRIEF:
[IGNY8_BUSINESS_BRIEF]
PRIMARY OBJECTIVES:
[IGNY8_OBJECTIVES]
STYLE & BRAND NOTES:
[IGNY8_STYLE]
SITE INFO / CURRENT STRUCTURE:
[IGNY8_SITE_INFO]
OUTPUT REQUIREMENTS
====================
Return ONE JSON object with the following keys:
{
"site": {
"name": "...",
"primary_navigation": ["home", "services", "about", "contact"],
"secondary_navigation": ["blog", "faq"],
"hero_message": "High level value statement",
"tone": "voice + tone summary"
},
"pages": [
{
"slug": "home",
"title": "Home",
"type": "home | about | services | products | blog | contact | custom",
"status": "draft",
"objective": "Explain the core brand promise and primary CTA",
"primary_cta": "Book a strategy call",
"seo": {
"meta_title": "...",
"meta_description": "..."
},
"blocks": [
{
"type": "hero | features | services | stats | testimonials | faq | contact | custom",
"heading": "Section headline",
"subheading": "Support copy",
"layout": "full-width | two-column | cards | carousel",
"content": [
"Bullet or short paragraph describing what to render in this block"
]
}
]
}
]
}
RULES
=====
- Include 58 pages covering the complete buyer journey (awareness → evaluation → conversion → trust).
- Every page must have at least 3 blocks with concrete guidance (no placeholders like "Lorem ipsum").
- Use consistent slug naming, all lowercase with hyphens.
- Type must match the allowed enum and reflect page intent.
- Ensure the navigation arrays align with the page list.
- Focus on practical descriptions that an engineering team can hand off directly to the Site Builder.
Return ONLY valid JSON. No commentary, explanations, or Markdown.
""",
'image_prompt_extraction': """Extract image prompts from the following article content.
ARTICLE TITLE: {title}
ARTICLE CONTENT:
{content}
Extract image prompts for:
1. Featured Image: One main image that represents the article topic
2. In-Article Images: Up to {max_images} images that would be useful within the article content
Return a JSON object with this structure:
{{
"featured_prompt": "Detailed description of the featured image",
"in_article_prompts": [
"Description of first in-article image",
"Description of second in-article image",
...
]
}}
Make sure each prompt is detailed enough for image generation, describing the visual elements, style, mood, and composition.""",
'image_prompt_template': '{image_type} image for blog post titled "{post_title}": {image_prompt}',
'negative_prompt': 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title',
'optimize_content': """You are an expert content optimizer specializing in SEO, readability, and engagement.
Your task is to optimize the provided content to improve its SEO score, readability, and engagement metrics.
CURRENT CONTENT:
Title: {CONTENT_TITLE}
Word Count: {WORD_COUNT}
Source: {SOURCE}
Primary Keyword: {PRIMARY_KEYWORD}
Internal Links: {INTERNAL_LINKS_COUNT}
CURRENT META DATA:
Meta Title: {META_TITLE}
Meta Description: {META_DESCRIPTION}
CURRENT SCORES:
{CURRENT_SCORES}
HTML CONTENT:
{HTML_CONTENT}
OPTIMIZATION REQUIREMENTS:
1. SEO Optimization:
- Ensure meta title is 30-60 characters (if provided)
- Ensure meta description is 120-160 characters (if provided)
- Optimize primary keyword usage (natural, not keyword stuffing)
- Improve heading structure (H1, H2, H3 hierarchy)
- Add internal links where relevant (maintain existing links)
2. Readability:
- Average sentence length: 15-20 words
- Use clear, concise language
- Break up long paragraphs
- Use bullet points and lists where appropriate
- Ensure proper paragraph structure
3. Engagement:
- Add compelling headings
- Include relevant images placeholders (alt text)
- Use engaging language
- Create clear call-to-action sections
- Improve content flow and structure
OUTPUT FORMAT:
Return ONLY a JSON object in this format:
{{
"html_content": "[Optimized HTML content]",
"meta_title": "[Optimized meta title, 30-60 chars]",
"meta_description": "[Optimized meta description, 120-160 chars]",
"optimization_notes": "[Brief notes on what was optimized]"
}}
Do not include any explanations, text, or commentary outside the JSON output.
""",
# Phase 8: Universal Content Types
'product_generation': """You are a product content specialist. Generate comprehensive product content that includes detailed descriptions, features, specifications, pricing, and benefits.
INPUT:
Product Name: [IGNY8_PRODUCT_NAME]
Product Description: [IGNY8_PRODUCT_DESCRIPTION]
Product Features: [IGNY8_PRODUCT_FEATURES]
Target Audience: [IGNY8_TARGET_AUDIENCE]
Primary Keyword: [IGNY8_PRIMARY_KEYWORD]
OUTPUT FORMAT:
Return ONLY a JSON object in this format:
{
"title": "[Product name and key benefit]",
"meta_title": "[SEO-optimized meta title, 30-60 chars]",
"meta_description": "[Compelling meta description, 120-160 chars]",
"html_content": "[Complete HTML product page content]",
"word_count": [Integer word count],
"primary_keyword": "[Primary keyword]",
"secondary_keywords": ["keyword1", "keyword2", "keyword3"],
"tags": ["tag1", "tag2", "tag3"],
"categories": ["Category > Subcategory"],
"json_blocks": [
{
"type": "product_overview",
"heading": "Product Overview",
"content": "Detailed product description"
},
{
"type": "features",
"heading": "Key Features",
"items": ["Feature 1", "Feature 2", "Feature 3"]
},
{
"type": "specifications",
"heading": "Specifications",
"data": {"Spec 1": "Value 1", "Spec 2": "Value 2"}
},
{
"type": "pricing",
"heading": "Pricing",
"content": "Pricing information"
},
{
"type": "benefits",
"heading": "Benefits",
"items": ["Benefit 1", "Benefit 2", "Benefit 3"]
}
],
"structure_data": {
"product_type": "[Product type]",
"price_range": "[Price range]",
"target_market": "[Target market]"
}
}
CONTENT REQUIREMENTS:
- Include compelling product overview
- List key features with benefits
- Provide detailed specifications
- Include pricing information (if available)
- Highlight unique selling points
- Use SEO-optimized headings
- Include call-to-action sections
- Ensure natural keyword usage
""",
'service_generation': """You are a service page content specialist. Generate comprehensive service page content that explains services, benefits, process, and pricing.
INPUT:
Service Name: [IGNY8_SERVICE_NAME]
Service Description: [IGNY8_SERVICE_DESCRIPTION]
Service Benefits: [IGNY8_SERVICE_BENEFITS]
Target Audience: [IGNY8_TARGET_AUDIENCE]
Primary Keyword: [IGNY8_PRIMARY_KEYWORD]
OUTPUT FORMAT:
Return ONLY a JSON object in this format:
{
"title": "[Service name and value proposition]",
"meta_title": "[SEO-optimized meta title, 30-60 chars]",
"meta_description": "[Compelling meta description, 120-160 chars]",
"html_content": "[Complete HTML service page content]",
"word_count": [Integer word count],
"primary_keyword": "[Primary keyword]",
"secondary_keywords": ["keyword1", "keyword2", "keyword3"],
"tags": ["tag1", "tag2", "tag3"],
"categories": ["Category > Subcategory"],
"json_blocks": [
{
"type": "service_overview",
"heading": "Service Overview",
"content": "Detailed service description"
},
{
"type": "benefits",
"heading": "Benefits",
"items": ["Benefit 1", "Benefit 2", "Benefit 3"]
},
{
"type": "process",
"heading": "Our Process",
"steps": ["Step 1", "Step 2", "Step 3"]
},
{
"type": "pricing",
"heading": "Pricing",
"content": "Pricing information"
},
{
"type": "faq",
"heading": "Frequently Asked Questions",
"items": [{"question": "Q1", "answer": "A1"}]
}
],
"structure_data": {
"service_type": "[Service type]",
"duration": "[Service duration]",
"target_market": "[Target market]"
}
}
CONTENT REQUIREMENTS:
- Clear service overview and value proposition
- Detailed benefits and outcomes
- Step-by-step process explanation
- Pricing information (if available)
- FAQ section addressing common questions
- Include testimonials or case studies (if applicable)
- Use SEO-optimized headings
- Include call-to-action sections
""",
'taxonomy_generation': """You are a taxonomy and categorization specialist. Generate comprehensive taxonomy page content that organizes and explains categories, tags, and hierarchical structures.
INPUT:
Taxonomy Name: [IGNY8_TAXONOMY_NAME]
Taxonomy Description: [IGNY8_TAXONOMY_DESCRIPTION]
Taxonomy Items: [IGNY8_TAXONOMY_ITEMS]
Primary Keyword: [IGNY8_PRIMARY_KEYWORD]
OUTPUT FORMAT:
Return ONLY a JSON object in this format:
{{
"title": "[Taxonomy name and purpose]",
"meta_title": "[SEO-optimized meta title, 30-60 chars]",
"meta_description": "[Compelling meta description, 120-160 chars]",
"html_content": "[Complete HTML taxonomy page content]",
"word_count": [Integer word count],
"primary_keyword": "[Primary keyword]",
"secondary_keywords": ["keyword1", "keyword2", "keyword3"],
"tags": ["tag1", "tag2", "tag3"],
"categories": ["Category > Subcategory"],
"json_blocks": [
{{
"type": "taxonomy_overview",
"heading": "Taxonomy Overview",
"content": "Detailed taxonomy description"
}},
{{
"type": "categories",
"heading": "Categories",
"items": [
{{
"name": "Category 1",
"description": "Category description",
"subcategories": ["Subcat 1", "Subcat 2"]
}}
]
}},
{{
"type": "tags",
"heading": "Tags",
"items": ["Tag 1", "Tag 2", "Tag 3"]
}},
{{
"type": "hierarchy",
"heading": "Taxonomy Hierarchy",
"structure": {{"Level 1": {{"Level 2": ["Level 3"]}}}}
}}
],
"structure_data": {{
"taxonomy_type": "[Taxonomy type]",
"item_count": [Integer],
"hierarchy_levels": [Integer]
}}
}}
CONTENT REQUIREMENTS:
- Clear taxonomy overview and purpose
- Organized category structure
- Tag organization and relationships
- Hierarchical structure visualization
- SEO-optimized headings
- Include navigation and organization benefits
- Use clear, descriptive language
""",
}
# 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 = {
@@ -622,7 +50,7 @@ CONTENT REQUIREMENTS:
Priority:
1. task.prompt_override (if task provided and has override)
2. DB prompt for (account, function)
3. Default fallback from registry
3. GlobalAIPrompt (REQUIRED - no hardcoded fallbacks)
Args:
function_name: AI function name (e.g., 'auto_cluster', 'generate_ideas')
@@ -642,7 +70,7 @@ CONTENT REQUIREMENTS:
# Step 2: Get prompt type
prompt_type = cls.FUNCTION_TO_PROMPT_TYPE.get(function_name, function_name)
# Step 3: Try DB prompt
# Step 3: Try DB prompt (account-specific)
if account:
try:
from igny8_core.modules.system.models import AIPrompt
@@ -651,18 +79,30 @@ CONTENT REQUIREMENTS:
prompt_type=prompt_type,
is_active=True
)
logger.info(f"Using DB prompt for {function_name} (account {account.id})")
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 DB prompt found for {function_name}: {e}")
logger.debug(f"No account-specific prompt found for {function_name}: {e}")
# Step 4: Use default fallback
prompt = cls.DEFAULT_PROMPTS.get(prompt_type, '')
if not prompt:
logger.warning(f"No default prompt found for {prompt_type}, using empty string")
return cls._render_prompt(prompt, context or {})
# 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)
@classmethod
def _render_prompt(cls, prompt_template: str, context: Dict[str, Any]) -> str:
@@ -728,8 +168,17 @@ CONTENT REQUIREMENTS:
except Exception:
pass
# Use default
return cls.DEFAULT_PROMPTS.get(prompt_type, '')
# Try GlobalAIPrompt
try:
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=prompt_type,
is_active=True
)
return global_prompt.prompt_value
except Exception:
# Fallback for image_prompt_template
return '{image_type} image for blog post titled "{post_title}": {image_prompt}'
@classmethod
def get_negative_prompt(cls, account: Optional[Any] = None) -> str:
@@ -752,8 +201,17 @@ CONTENT REQUIREMENTS:
except Exception:
pass
# Use default
return cls.DEFAULT_PROMPTS.get(prompt_type, '')
# Try GlobalAIPrompt
try:
from igny8_core.modules.system.global_settings_models import GlobalAIPrompt
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=prompt_type,
is_active=True
)
return global_prompt.prompt_value
except Exception:
# Fallback for negative_prompt
return 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title'
# Convenience function for backward compatibility

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-20 15:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('automation', '0004_add_pause_resume_cancel_fields'),
]
operations = [
migrations.AlterField(
model_name='automationconfig',
name='stage_1_batch_size',
field=models.IntegerField(default=50, help_text='Keywords per batch'),
),
]

View File

@@ -343,18 +343,22 @@ class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
}),
("DALL-E Settings", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size", "dalle_quality", "dalle_style"),
"description": "Global DALL-E image generation configuration"
("Image Generation - Default Service", {
"fields": ("default_image_service",),
"description": "Choose which image generation service is used by default for all accounts"
}),
("Anthropic Settings", {
"fields": ("anthropic_api_key", "anthropic_model"),
"description": "Global Anthropic Claude configuration"
("Image Generation - DALL-E", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size"),
"description": "Global DALL-E (OpenAI) image generation configuration"
}),
("Runware Settings", {
"fields": ("runware_api_key",),
("Image Generation - Runware", {
"fields": ("runware_api_key", "runware_model"),
"description": "Global Runware image generation configuration"
}),
("Universal Image Settings", {
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size", "mobile_image_size"),
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
}),
("Status", {
"fields": ("is_active", "last_updated", "updated_by")
}),

View File

@@ -20,6 +20,61 @@ class GlobalIntegrationSettings(models.Model):
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
"""
OPENAI_MODEL_CHOICES = [
('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'),
('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'),
('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'),
('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'),
('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'),
('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)'),
]
DALLE_MODEL_CHOICES = [
('dall-e-3', 'DALL·E 3 - $0.040 per image'),
('dall-e-2', 'DALL·E 2 - $0.020 per image'),
]
DALLE_SIZE_CHOICES = [
('1024x1024', '1024x1024 (Square)'),
('1792x1024', '1792x1024 (Landscape)'),
('1024x1792', '1024x1792 (Portrait)'),
('512x512', '512x512 (Small Square)'),
]
DALLE_QUALITY_CHOICES = [
('standard', 'Standard'),
('hd', 'HD'),
]
DALLE_STYLE_CHOICES = [
('vivid', 'Vivid'),
('natural', 'Natural'),
]
RUNWARE_MODEL_CHOICES = [
('runware:97@1', 'Runware 97@1 - Versatile Model'),
('runware:100@1', 'Runware 100@1 - High Quality'),
('runware:101@1', 'Runware 101@1 - Fast Generation'),
]
IMAGE_QUALITY_CHOICES = [
('standard', 'Standard'),
('hd', 'HD'),
]
IMAGE_STYLE_CHOICES = [
('vivid', 'Vivid'),
('natural', 'Natural'),
('realistic', 'Realistic'),
('artistic', 'Artistic'),
('cartoon', 'Cartoon'),
]
IMAGE_SERVICE_CHOICES = [
('openai', 'OpenAI DALL-E'),
('runware', 'Runware'),
]
# OpenAI Settings (for text generation)
openai_api_key = models.CharField(
max_length=500,
@@ -28,7 +83,8 @@ class GlobalIntegrationSettings(models.Model):
)
openai_model = models.CharField(
max_length=100,
default='gpt-4-turbo-preview',
default='gpt-4o-mini',
choices=OPENAI_MODEL_CHOICES,
help_text="Default text generation model (accounts can override if plan allows)"
)
openai_temperature = models.FloatField(
@@ -40,7 +96,7 @@ class GlobalIntegrationSettings(models.Model):
help_text="Default max tokens for responses (accounts can override if plan allows)"
)
# DALL-E Settings (for image generation)
# Image Generation Settings (OpenAI/DALL-E)
dalle_api_key = models.CharField(
max_length=500,
blank=True,
@@ -49,44 +105,64 @@ class GlobalIntegrationSettings(models.Model):
dalle_model = models.CharField(
max_length=100,
default='dall-e-3',
choices=DALLE_MODEL_CHOICES,
help_text="Default DALL-E model (accounts can override if plan allows)"
)
dalle_size = models.CharField(
max_length=20,
default='1024x1024',
choices=DALLE_SIZE_CHOICES,
help_text="Default image size (accounts can override if plan allows)"
)
dalle_quality = models.CharField(
max_length=20,
default='standard',
choices=[('standard', 'Standard'), ('hd', 'HD')],
help_text="Default image quality (accounts can override if plan allows)"
)
dalle_style = models.CharField(
max_length=20,
default='vivid',
choices=[('vivid', 'Vivid'), ('natural', 'Natural')],
help_text="Default image style (accounts can override if plan allows)"
)
# Anthropic Settings (for Claude)
anthropic_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Anthropic API key - used by ALL accounts"
)
anthropic_model = models.CharField(
max_length=100,
default='claude-3-sonnet-20240229',
help_text="Default Anthropic model (accounts can override if plan allows)"
)
# Runware Settings (alternative image generation)
# Image Generation Settings (Runware)
runware_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Runware API key - used by ALL accounts"
)
runware_model = models.CharField(
max_length=100,
default='runware:97@1',
choices=RUNWARE_MODEL_CHOICES,
help_text="Default Runware model (accounts can override if plan allows)"
)
# Default Image Generation Service
default_image_service = models.CharField(
max_length=20,
default='openai',
choices=IMAGE_SERVICE_CHOICES,
help_text="Default image generation service for all accounts (openai=DALL-E, runware=Runware)"
)
# Universal Image Generation Settings (applies to ALL providers)
image_quality = models.CharField(
max_length=20,
default='standard',
choices=IMAGE_QUALITY_CHOICES,
help_text="Default image quality for all providers (accounts can override if plan allows)"
)
image_style = models.CharField(
max_length=20,
default='realistic',
choices=IMAGE_STYLE_CHOICES,
help_text="Default image style for all providers (accounts can override if plan allows)"
)
max_in_article_images = models.IntegerField(
default=2,
help_text="Default maximum images to generate per article (1-5, accounts can override if plan allows)"
)
desktop_image_size = models.CharField(
max_length=20,
default='1024x1024',
help_text="Default desktop image size (accounts can override if plan allows)"
)
mobile_image_size = models.CharField(
max_length=20,
default='512x512',
help_text="Default mobile image size (accounts can override if plan allows)"
)
# Metadata
is_active = models.BooleanField(default=True)
@@ -151,7 +227,8 @@ class GlobalAIPrompt(models.Model):
description = models.TextField(blank=True, help_text="Description of what this prompt does")
variables = models.JSONField(
default=list,
help_text="List of variables used in the prompt (e.g., {keyword}, {industry})"
blank=True,
help_text="Optional: List of variables used in the prompt (e.g., {keyword}, {industry})"
)
is_active = models.BooleanField(default=True, db_index=True)
version = models.IntegerField(default=1, help_text="Prompt version for tracking changes")

View File

@@ -94,14 +94,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner])
def test_connection(self, request, pk=None):
"""
Test API connection for OpenAI or Runware
Supports two modes:
- with_response=false: Simple connection test (GET /v1/models)
- with_response=true: Full response test with ping message
Test API connection using platform API keys.
Tests OpenAI or Runware with current model selection.
"""
integration_type = pk # 'openai', 'runware'
logger.info(f"[test_connection] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
logger.info(f"[test_connection] Called for integration_type={integration_type}")
if not integration_type:
return error_response(
@@ -110,70 +108,43 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
request=request
)
# Get API key and config from request or saved settings
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
api_key = request.data.get('apiKey') or config.get('apiKey')
# Merge request.data with config if config is a dict
if not isinstance(config, dict):
config = {}
if not api_key:
# Try to get from saved settings
account = getattr(request, 'account', None)
logger.info(f"[test_connection] Account from request: {account.id if account else None}")
# Fallback to user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback to default account
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
if account:
try:
from .models import IntegrationSettings
logger.info(f"[test_connection] Looking for saved settings for account {account.id}")
saved_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account
)
api_key = saved_settings.config.get('apiKey')
logger.info(f"[test_connection] Found saved settings, has_apiKey={bool(api_key)}")
except IntegrationSettings.DoesNotExist:
logger.warning(f"[test_connection] No saved settings found for {integration_type} and account {account.id}")
pass
if not api_key:
logger.error(f"[test_connection] No API key found in request or saved settings")
return error_response(
error='API key is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})")
try:
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
# Get platform API keys
global_settings = GlobalIntegrationSettings.get_instance()
# Get config from request (model selection)
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
if integration_type == 'openai':
api_key = global_settings.openai_api_key
if not api_key:
return error_response(
error='Platform OpenAI API key not configured. Please contact administrator.',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return self._test_openai(api_key, config, request)
elif integration_type == 'runware':
api_key = global_settings.runware_api_key
if not api_key:
return error_response(
error='Platform Runware API key not configured. Please contact administrator.',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return self._test_runware(api_key, request)
else:
return error_response(
error=f'Validation not supported for {integration_type}',
error=f'Testing not supported for {integration_type}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
except Exception as e:
logger.error(f"Error testing {integration_type} connection: {str(e)}", exc_info=True)
import traceback
error_trace = traceback.format_exc()
logger.error(f"Full traceback: {error_trace}")
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -662,8 +633,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
return self.save_settings(request, integration_type)
def save_settings(self, request, pk=None):
"""Save integration settings"""
integration_type = pk # 'openai', 'runware', 'gsc'
"""
Save integration settings (account overrides only).
- Saves model/parameter overrides to IntegrationSettings
- NEVER saves API keys (those are platform-wide)
- Free plan: Should be blocked at frontend level
"""
integration_type = pk
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
@@ -678,12 +654,19 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
config = dict(request.data) if hasattr(request.data, 'dict') else (request.data if isinstance(request.data, dict) else {})
logger.info(f"[save_settings] Config keys: {list(config.keys()) if isinstance(config, dict) else 'Not a dict'}")
# Remove any API keys from config (security - they shouldn't be sent but just in case)
config.pop('apiKey', None)
config.pop('api_key', None)
config.pop('openai_api_key', None)
config.pop('dalle_api_key', None)
config.pop('runware_api_key', None)
config.pop('anthropic_api_key', None)
try:
# Get account - try multiple methods
# Get account
account = getattr(request, 'account', None)
logger.info(f"[save_settings] Account from request: {account.id if account else None}")
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
@@ -693,93 +676,81 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.warning(f"Error getting account from user: {e}")
account = None
# Fallback 2: If still no account, get default account (for development)
if not account:
from igny8_core.auth.models import Account
try:
# Get the first account as fallback (development only)
account = Account.objects.first()
except Exception as e:
logger.warning(f"Error getting default account: {e}")
account = None
if not account:
logger.error(f"[save_settings] No account found after all fallbacks")
logger.error(f"[save_settings] No account found")
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[save_settings] Using account: {account.id} ({account.name}, slug={account.slug}, status={account.status})")
logger.info(f"[save_settings] Using account: {account.id} ({account.name})")
# Store integration settings in a simple model or settings table
# For now, we'll use a simple approach - store in IntegrationSettings model
# or use Django settings/database
# TODO: Check if Free plan - they shouldn't be able to save overrides
# This should be blocked at frontend level, but add backend check too
# Import IntegrationSettings model
from .models import IntegrationSettings
# For image_generation, ensure provider is set correctly
if integration_type == 'image_generation':
# Build clean config with only allowed overrides
clean_config = {}
if integration_type == 'openai':
# Only allow model, temperature, max_tokens overrides
if 'model' in config:
clean_config['model'] = config['model']
if 'temperature' in config:
clean_config['temperature'] = config['temperature']
if 'max_tokens' in config:
clean_config['max_tokens'] = config['max_tokens']
elif integration_type == 'image_generation':
# Map service to provider if service is provided
if 'service' in config and 'provider' not in config:
config['provider'] = config['service']
# Ensure provider is set
if 'provider' not in config:
config['provider'] = config.get('service', 'openai')
# Set model based on provider
if config.get('provider') == 'openai' and 'model' not in config:
config['model'] = config.get('imageModel', 'dall-e-3')
elif config.get('provider') == 'runware' and 'model' not in config:
config['model'] = config.get('runwareModel', 'runware:97@1')
# Ensure all image settings have defaults (except max_in_article_images which must be explicitly set)
config.setdefault('image_type', 'realistic')
config.setdefault('image_format', 'webp')
config.setdefault('desktop_enabled', True)
config.setdefault('mobile_enabled', True)
# Set default image sizes based on provider/model
provider = config.get('provider', 'openai')
model = config.get('model', 'dall-e-3')
if not config.get('featured_image_size'):
if provider == 'runware':
config['featured_image_size'] = '1280x832'
else: # openai
config['featured_image_size'] = '1024x1024'
if not config.get('desktop_image_size'):
config['desktop_image_size'] = '1024x1024'
if 'service' in config:
clean_config['service'] = config['service']
clean_config['provider'] = config['service']
if 'provider' in config:
clean_config['provider'] = config['provider']
clean_config['service'] = config['provider']
# Model selection (service-specific)
if 'model' in config:
clean_config['model'] = config['model']
if 'imageModel' in config:
clean_config['imageModel'] = config['imageModel']
clean_config['model'] = config['imageModel'] # Also store in 'model' for consistency
if 'runwareModel' in config:
clean_config['runwareModel'] = config['runwareModel']
# Universal image settings (applies to all providers)
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
if key in config:
clean_config[key] = config[key]
# Get or create integration settings
logger.info(f"[save_settings] Attempting get_or_create for {integration_type} with account {account.id}")
logger.info(f"[save_settings] Saving clean config: {clean_config}")
integration_settings, created = IntegrationSettings.objects.get_or_create(
integration_type=integration_type,
account=account,
defaults={'config': config, 'is_active': config.get('enabled', False)}
defaults={'config': clean_config, 'is_active': True}
)
logger.info(f"[save_settings] get_or_create result: created={created}, id={integration_settings.id}")
logger.info(f"[save_settings] Result: created={created}, id={integration_settings.id}")
if not created:
logger.info(f"[save_settings] Updating existing settings (id={integration_settings.id})")
integration_settings.config = config
integration_settings.is_active = config.get('enabled', False)
integration_settings.config = clean_config
integration_settings.is_active = True
integration_settings.save()
logger.info(f"[save_settings] Settings updated successfully")
logger.info(f"[save_settings] Updated existing settings")
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
logger.info(f"[save_settings] Successfully saved overrides for {integration_type}")
return success_response(
data={'config': config},
data={'config': clean_config},
message=f'{integration_type.upper()} settings saved successfully',
request=request
)
except Exception as e:
logger.error(f"Error saving integration settings for {integration_type}: {str(e)}", exc_info=True)
import traceback
error_trace = traceback.format_exc()
logger.error(f"Full traceback: {error_trace}")
return error_response(
error=f'Failed to save settings: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -787,7 +758,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
)
def get_settings(self, request, pk=None):
"""Get integration settings - defaults to AWS-admin settings if account doesn't have its own"""
"""
Get integration settings for frontend.
Returns:
- Global defaults (model, temperature, etc.)
- Account overrides if they exist
- NO API keys (platform-wide only)
"""
integration_type = pk
if not integration_type:
@@ -798,10 +775,9 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
)
try:
# Get account - try multiple methods (same as save_settings)
# Get account
account = getattr(request, 'account', None)
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
@@ -812,31 +788,116 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
account = None
from .models import IntegrationSettings
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
# Get account-specific settings
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account
)
response_data = {
'id': integration_settings.integration_type,
'enabled': integration_settings.is_active,
**integration_settings.config
}
return success_response(
data=response_data,
request=request
)
except IntegrationSettings.DoesNotExist:
pass
except Exception as e:
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
# Get global defaults
global_settings = GlobalIntegrationSettings.get_instance()
# Build response with global defaults
if integration_type == 'openai':
response_data = {
'id': 'openai',
'enabled': True, # Always enabled (platform-wide)
'model': global_settings.openai_model,
'temperature': global_settings.openai_temperature,
'max_tokens': global_settings.openai_max_tokens,
'using_global': True, # Flag to show it's using global
}
# Check for account overrides
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account,
is_active=True
)
config = integration_settings.config or {}
if config.get('model'):
response_data['model'] = config['model']
response_data['using_global'] = False
if config.get('temperature') is not None:
response_data['temperature'] = config['temperature']
if config.get('max_tokens'):
response_data['max_tokens'] = config['max_tokens']
except IntegrationSettings.DoesNotExist:
pass
elif integration_type == 'runware':
response_data = {
'id': 'runware',
'enabled': True, # Always enabled (platform-wide)
'using_global': True,
}
elif integration_type == 'image_generation':
# Get default service and model based on global settings
default_service = global_settings.default_image_service
default_model = global_settings.dalle_model if default_service == 'openai' else global_settings.runware_model
response_data = {
'id': 'image_generation',
'enabled': True,
'service': default_service, # From global settings
'provider': default_service, # Alias for service
'model': default_model, # Service-specific default model
'imageModel': global_settings.dalle_model, # OpenAI model
'runwareModel': global_settings.runware_model, # Runware model
'image_type': global_settings.image_style, # Use image_style as default
'image_quality': global_settings.image_quality, # Universal quality
'image_style': global_settings.image_style, # Universal style
'max_in_article_images': global_settings.max_in_article_images,
'image_format': 'webp',
'desktop_enabled': True,
'mobile_enabled': True,
'featured_image_size': global_settings.dalle_size,
'desktop_image_size': global_settings.desktop_image_size,
'mobile_image_size': global_settings.mobile_image_size,
'using_global': True,
}
# Check for account overrides
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account,
is_active=True
)
config = integration_settings.config or {}
# Override with account settings
if config:
response_data['using_global'] = False
# Service/provider
if 'service' in config:
response_data['service'] = config['service']
response_data['provider'] = config['service']
if 'provider' in config:
response_data['provider'] = config['provider']
response_data['service'] = config['provider']
# Models
if 'model' in config:
response_data['model'] = config['model']
if 'imageModel' in config:
response_data['imageModel'] = config['imageModel']
if 'runwareModel' in config:
response_data['runwareModel'] = config['runwareModel']
# Universal image settings
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
if key in config:
response_data[key] = config[key]
except IntegrationSettings.DoesNotExist:
pass
else:
# Other integration types - return empty
response_data = {
'id': integration_type,
'enabled': False,
}
# Return empty config if no settings found
return success_response(
data={},
data=response_data,
request=request
)
except Exception as e:

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0003_fix_global_settings_architecture'),
('system', '0002_add_global_settings_models'),
]
operations = [

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.9 on 2025-12-20 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0004_fix_global_settings_remove_override'),
]
operations = [
migrations.AlterField(
model_name='globalintegrationsettings',
name='anthropic_model',
field=models.CharField(choices=[('claude-3-5-sonnet-20241022', 'Claude 3.5 Sonnet (Oct 2024) - $3.00 / $15.00 per 1M tokens'), ('claude-3-5-sonnet-20240620', 'Claude 3.5 Sonnet (Jun 2024) - $3.00 / $15.00 per 1M tokens'), ('claude-3-opus-20240229', 'Claude 3 Opus - $15.00 / $75.00 per 1M tokens'), ('claude-3-sonnet-20240229', 'Claude 3 Sonnet - $3.00 / $15.00 per 1M tokens'), ('claude-3-haiku-20240307', 'Claude 3 Haiku - $0.25 / $1.25 per 1M tokens')], default='claude-3-5-sonnet-20241022', help_text='Default Anthropic model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_model',
field=models.CharField(choices=[('dall-e-3', 'DALL·E 3 - $0.040 per image'), ('dall-e-2', 'DALL·E 2 - $0.020 per image')], default='dall-e-3', help_text='Default DALL-E model (accounts can override if plan allows)', max_length=100),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_size',
field=models.CharField(choices=[('1024x1024', '1024x1024 (Square)'), ('1792x1024', '1792x1024 (Landscape)'), ('1024x1792', '1024x1792 (Portrait)'), ('512x512', '512x512 (Small Square)')], default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_model',
field=models.CharField(choices=[('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'), ('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'), ('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'), ('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'), ('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'), ('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)')], default='gpt-4o-mini', help_text='Default text generation model (accounts can override if plan allows)', max_length=100),
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.2.9 on 2025-12-20 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0005_add_model_choices'),
]
operations = [
migrations.RemoveField(
model_name='globalintegrationsettings',
name='anthropic_api_key',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='anthropic_model',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='dalle_quality',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='dalle_style',
),
migrations.AddField(
model_name='globalintegrationsettings',
name='image_quality',
field=models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality for all providers (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='image_style',
field=models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural'), ('realistic', 'Realistic'), ('artistic', 'Artistic'), ('cartoon', 'Cartoon')], default='realistic', help_text='Default image style for all providers (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='runware_model',
field=models.CharField(choices=[('runware:97@1', 'Runware 97@1 - Versatile Model'), ('runware:100@1', 'Runware 100@1 - High Quality'), ('runware:101@1', 'Runware 101@1 - Fast Generation')], default='runware:97@1', help_text='Default Runware model (accounts can override if plan allows)', max_length=100),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.9 on 2025-12-20 15:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0006_fix_image_settings'),
]
operations = [
migrations.AddField(
model_name='globalintegrationsettings',
name='desktop_image_size',
field=models.CharField(default='1024x1024', help_text='Default desktop image size (accounts can override if plan allows)', max_length=20),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='max_in_article_images',
field=models.IntegerField(default=2, help_text='Default maximum images to generate per article (1-5, accounts can override if plan allows)'),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='mobile_image_size',
field=models.CharField(default='512x512', help_text='Default mobile image size (accounts can override if plan allows)', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-20 15:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0007_add_image_defaults'),
]
operations = [
migrations.AddField(
model_name='globalintegrationsettings',
name='default_image_service',
field=models.CharField(choices=[('openai', 'OpenAI DALL-E'), ('runware', 'Runware')], default='openai', help_text='Default image generation service for all accounts (openai=DALL-E, runware=Runware)', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-20 19:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0008_add_default_image_service'),
]
operations = [
migrations.AlterField(
model_name='globalaiprompt',
name='variables',
field=models.JSONField(blank=True, default=list, help_text='Optional: List of variables used in the prompt (e.g., {keyword}, {industry})'),
),
]

View File

@@ -84,11 +84,23 @@ class AIPrompt(AccountBaseModel):
return None
def reset_to_default(self):
"""Reset prompt to global default"""
if self.default_prompt:
self.prompt_value = self.default_prompt
"""Reset prompt to global default from GlobalAIPrompt"""
from .global_settings_models import GlobalAIPrompt
try:
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=self.prompt_type,
is_active=True
)
self.prompt_value = global_prompt.prompt_value
self.default_prompt = global_prompt.prompt_value
self.is_customized = False
self.save()
except GlobalAIPrompt.DoesNotExist:
raise ValueError(
f"Cannot reset: Global prompt '{self.prompt_type}' not found. "
f"Please configure it in Django admin at: /admin/system/globalaiprompt/"
)
def __str__(self):
status = "Custom" if self.is_customized else "Default"

View File

@@ -5,8 +5,14 @@ from typing import Optional
def get_default_prompt(prompt_type: str) -> str:
"""Get default prompt value by type"""
defaults = {
"""Get default prompt value from GlobalAIPrompt ONLY - single source of truth"""
from .global_settings_models import GlobalAIPrompt
try:
global_prompt = GlobalAIPrompt.objects.get(prompt_type=prompt_type, is_active=True)
return global_prompt.prompt_value
except GlobalAIPrompt.DoesNotExist:
return f"ERROR: Global prompt '{prompt_type}' not configured in admin. Please configure it at: admin/system/globalaiprompt/"
'clustering': """You are a semantic strategist and SEO architecture engine. Your task is to analyze the provided keyword list and group them into meaningful, intent-driven topic clusters that reflect how real users search, think, and act online.
Return a single JSON object with a "clusters" array. Each cluster must follow this structure:

View File

@@ -46,33 +46,20 @@ export default function ValidationCard({
setTestResult(null);
try {
// Get saved settings to get API key and model
// Get saved settings to get model
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So settingsData IS the data object (config object)
const settingsData = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/`);
let apiKey = '';
let model = 'gpt-4.1';
let model = 'gpt-4o-mini';
if (settingsData && typeof settingsData === 'object') {
apiKey = settingsData.apiKey || '';
model = settingsData.model || 'gpt-4.1';
model = settingsData.model || 'gpt-4o-mini';
}
if (!apiKey) {
setTestResult({
success: false,
message: 'API key not configured. Please configure your API key in settings first.',
});
setIsLoading(false);
return;
}
// Call test endpoint
// Call test endpoint (uses platform API key - no apiKey parameter needed)
// For Runware, we don't need with_response or model config
const requestBody: any = {
apiKey: apiKey,
};
const requestBody: any = {};
if (integrationId === 'openai') {
requestBody.config = {

View File

@@ -17,7 +17,6 @@ import {
FileIcon,
UserIcon,
UserCircleIcon,
BoxCubeIcon,
} from "../icons";
import { useSidebar } from "../context/SidebarContext";
import SidebarWidget from "./SidebarWidget";
@@ -216,36 +215,10 @@ const AppSidebar: React.FC = () => {
name: "Profile Settings",
path: "/settings/profile",
},
// Integration is admin-only; hide for non-privileged users (handled in render)
{
icon: <PlugInIcon />,
name: "Integration",
name: "AI Model Settings",
path: "/settings/integration",
adminOnly: true,
},
// Global Settings - Admin only, dropdown with global config pages
{
icon: <BoxCubeIcon />,
name: "Global Settings",
adminOnly: true,
subItems: [
{
name: "Platform API Keys",
path: "/admin/system/globalintegrationsettings/",
},
{
name: "Global Prompts",
path: "/admin/system/globalaiprompt/",
},
{
name: "Global Author Profiles",
path: "/admin/system/globalauthorprofile/",
},
{
name: "Global Strategies",
path: "/admin/system/globalstrategy/",
},
],
},
{
icon: <PageIcon />,

View File

@@ -411,23 +411,24 @@ export default function Integration() {
if (integrationId === 'openai') {
return [
{ label: 'App Name', value: 'OpenAI API' },
{ label: 'API Key', value: config.apiKey ? `${config.apiKey.substring(0, 20)}...` : 'Not configured' },
{ label: 'Model', value: config.model || 'Not set' },
{ label: 'Model', value: config.model || 'gpt-4o-mini' },
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
];
} else if (integrationId === 'runware') {
return [
{ label: 'App Name', value: 'Runware API' },
{ label: 'API Key', value: config.apiKey ? `${config.apiKey.substring(0, 20)}...` : 'Not configured' },
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
];
} else if (integrationId === 'image_generation') {
const service = config.service || 'openai';
const modelDisplay = service === 'openai'
? (config.model || 'Not set')
: (config.runwareModel || 'Not set');
? (config.model || config.imageModel || 'dall-e-3')
: (config.runwareModel || 'runware:97@1');
return [
{ label: 'Service', value: service === 'openai' ? 'OpenAI' : 'Runware' },
{ label: 'Service', value: service === 'openai' ? 'OpenAI DALL-E' : 'Runware' },
{ label: 'Model', value: modelDisplay },
{ label: 'Status', value: config.using_global ? 'Using platform defaults' : 'Custom settings' },
];
}
return [];