globals
This commit is contained in:
223
INTEGRATION-SETTINGS-WORKFLOW.md
Normal file
223
INTEGRATION-SETTINGS-WORKFLOW.md
Normal 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
|
||||
@@ -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'),
|
||||
|
||||
@@ -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": "[1–2 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 2–3 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 3–10 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 + 2–4 supporting ideas.
|
||||
Each idea must include:
|
||||
title, description, content_type, content_structure, cluster_id, estimated_word_count (1500–2200), and covered_keywords.
|
||||
|
||||
Outline Rules:
|
||||
|
||||
Intro: 1 hook (30–40 words) + 2 intro paragraphs (50–60 words each).
|
||||
|
||||
5–8 H2 sections, each with 2–3 H3s.
|
||||
|
||||
Each H2 ≈ 250–300 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 (30–40 words)
|
||||
- Follow with 2 narrative paragraphs (each 50–60 words; 2–3 sentences max)
|
||||
- No headings allowed in intro
|
||||
|
||||
**H2 SECTIONS (5–8 total):**
|
||||
Each section should be 250–300 words and follow this format:
|
||||
1. Two narrative paragraphs (80–120 words each, 2–3 sentences)
|
||||
2. One list or table (must come *after* a paragraph)
|
||||
3. Optional closing paragraph (40–60 words)
|
||||
4. Insert 2–3 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 5–8 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
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
}),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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})'),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user