commit 6fd7213076c9bf8faa7ef4811d193b4cd2df8f40 Author: Michael Ikehi Date: Thu Apr 17 08:50:12 2025 +0100 feat: Initial implementation of Marketing Assistant AI for Adriana James - Set up FastAPI backend with modular structure: - main.py for API routing - copywriter.py for AI-powered content generation using Cohere - embeddings.py for generating and reranking content embeddings - vector_store.py for FAISS-based similarity search - brand_style.py for managing brand tone, taboo words, and preferred terms - config.py for managing environment and application settings - Configured RESTful API endpoints: /generate-copy, /brand-style, /training-data, /improve-content, /analyze-content - Created frontend with vanilla HTML, CSS, and JS (index.html, styles.css, app.js) - Integrated brand style management for tone, voice, taboo words, and terminology - Implemented vector search for referencing similar historical content - Enabled training data input to improve future AI output - Added environment variable support for API keys and model configs - Structured data storage with local JSON and DB files - Added developer documentation, API reference, and project setup instructions This commit provides the foundation for a full-stack, AI-driven content creation platform that ensures brand consistency, speeds up marketing workflows, and supports iterative improvement over time. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa2ff0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment variables +.env + +# Virtual Environment +venv/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Local data +data/past_campaigns/* +data/user_queries/* +!data/past_campaigns/.gitkeep +!data/user_queries/.gitkeep + +# OS specific +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..c83ee5e --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Adriana James + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dbd6d1 --- /dev/null +++ b/README.md @@ -0,0 +1,247 @@ +# Marketing Assistant AI + +A comprehensive AI-powered tool designed to streamline the process of ideation, copywriting, and marketing campaign creation for Adriana James. This system generates high-quality marketing content that maintains brand consistency while significantly reducing content creation time. + + +## 🌟 Features + +- **AI-Powered Content Generation**: Create marketing content for various channels with customizable parameters +- **Brand Voice Consistency**: Ensure all content adheres to Adriana James' unique brand tone and style +- **Vector Search**: Find similar past content for reference and inspiration +- **Content Improvement**: Refine generated content based on specific feedback +- **Brand Style Management**: Configure and update brand guidelines through a user-friendly interface +- **Training Data Management**: Add successful marketing content to improve the AI over time +- **Content Performance Analysis**: Get insights into the potential effectiveness of your content + +## 📋 Project Overview + +The Marketing Assistant AI combines a powerful backend built with FastAPI and a clean, intuitive frontend interface. It leverages advanced AI models through the Cohere API for text generation, embeddings, and reranking. + +### Tech Stack + +- **Backend**: Python with FastAPI +- **Frontend**: Vanilla HTML, CSS, and JavaScript +- **AI Models**: Cohere's generation and embedding models +- **Vector Database**: FAISS for efficient content similarity search +- **Storage**: Local JSON files for historical marketing data + +## 🚀 Getting Started + +### Prerequisites + +- Python 3.9+ +- Node.js and npm (optional, for development tools) +- Cohere API key + +### Installation + +1. **Clone the repository** + +```bash +git clone http://23.29.118.76:3000/michael/marketing-assistant-ai.git +cd marketing-assistant-ai +``` + +2. **Set up the backend** + +```bash +cd backend +pip install -r requirements.txt +``` + +3. **Configure environment variables** + +Create a `.env` file in the project root with the following variables: + +``` +# API Keys +COHERE_API_KEY=your-api-key-here + +# LLM Configuration +LLM_MODEL=llm-model-name # e.g., command +LLM_API_KEY=your-api-key-here # if using a custom model + +# Server Configuration +API_HOST=localhost +API_PORT=8000 + +# Database Configuration +VECTOR_DB_PATH=./data/vector_store +HISTORY_DB_PATH=./data/history.db + +# Brand Configuration +BRAND_NAME=Adriana James +``` + +4. **Start the backend server** + +```bash +cd backend +python main.py +``` + +The API will be available at http://localhost:8000 + +5. **Serve the frontend** + +You can use any static file server. For development, Python's built-in HTTP server works well: + +```bash +cd frontend +python -m http.server 3000 +``` + +The frontend will be available at http://localhost:3000 + +## 🔧 Usage + +### Generating Content + +1. Navigate to the "Generate" tab in the web interface +2. Enter your content prompt (e.g., "Write a social media post about our new coaching program") +3. Select optional parameters (content type, tone, length) +4. Click "Generate" and wait for the AI to create your content +5. Review, improve, and save the generated content + +### Managing Brand Style + +1. Go to the "Brand Style" tab +2. Configure tone options and voice characteristics +3. Add or remove taboo words +4. Define preferred terminology +5. Save the updated brand style + +### Adding Training Data + +1. Visit the "Training" tab +2. Select the content type +3. Enter your high-performing marketing content +4. Add performance metrics if available +5. Submit the training data to improve the AI over time + +## 📁 Project Structure + +``` +Marketing_Assistant_AI/ +│-- backend/ +│ │-- main.py # FastAPI application +│ │-- copywriter.py # AI content generation +│ │-- vector_store.py # FAISS vector database +│ │-- embeddings.py # Cohere embeddings +│ │-- brand_style.py # Brand style management +│ │-- config.py # Configuration settings +│ │-- requirements.txt # Python dependencies +│ +│-- data/ +│ │-- past_campaigns/ # Stored marketing campaigns +│ │-- user_queries/ # Query history +│ │-- style_guidelines/ # Brand style config +│ │ │-- brand_style.json # Default style guidelines +│ +│-- frontend/ +│ │-- index.html # Main HTML page +│ │-- styles.css # CSS styles +│ │-- app.js # Frontend functionality +│ +│-- docs/ +│ │-- README.md # Developer documentation +│ │-- API_Documentation.md # API reference +│ +│-- .env # Environment variables +│-- .gitignore # Git ignore rules +│-- LICENSE # License information +│-- README.md # This file +``` + +## 🔄 API Endpoints + +The backend provides several RESTful API endpoints: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/generate-copy` | POST | Generate marketing content | +| `/brand-style` | GET | Retrieve brand style guidelines | +| `/brand-style` | PUT | Update brand style guidelines | +| `/training-data` | POST | Add new training data | +| `/training-data` | GET | List available training data | +| `/training-data/{id}` | GET | Get specific training document | +| `/training-data/{id}` | DELETE | Delete training document | +| `/improve-content` | POST | Improve content based on feedback | +| `/analyze-content` | POST | Analyze content performance | + +For detailed API documentation, see [API Documentation](docs/API_Documentation.md). + +## 💡 How It Works + +1. **Content Generation Process** + - User submits a content request + - System formats the prompt with brand style guidelines + - Similar past content is retrieved from the vector database + - The AI generates content using the formatted prompt and references + - The system checks alignment with brand guidelines + - Final content is returned with alignment score and suggestions + +2. **Brand Style Management** + - Brand style guidelines are stored in JSON format + - The system uses these guidelines to format prompts for the AI + - Content is checked against taboo words and preferred terminology + - An alignment score is calculated based on adherence to guidelines + +3. **Vector Search** + - Marketing content is converted to vector embeddings using Cohere + - FAISS enables efficient similarity search across thousands of documents + - Similar content is reranked based on relevance to the query + +## 🛠️ Development + +### Backend Development + +The backend is built with FastAPI and follows a modular architecture: + +- `main.py`: API endpoints and request handling +- `copywriter.py`: Core AI content generation +- `vector_store.py`: Vector database operations +- `embeddings.py`: Embedding generation and reranking +- `brand_style.py`: Brand style management +- `config.py`: Configuration settings + +### Frontend Development + +The frontend is built with vanilla HTML, CSS, and JavaScript: + +- `index.html`: Main application structure and UI components +- `styles.css`: Styling and responsive design rules +- `app.js`: User interactions and API integration + +## 🔍 Future Enhancements + +- User authentication and role-based access +- Content performance analytics dashboard +- A/B testing capabilities +- Integration with marketing platforms +- Custom model fine-tuning for even better results +- Mobile application + +## 👥 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 👏 Acknowledgements + +- [Cohere](https://cohere.ai/) for AI models +- [FastAPI](https://fastapi.tiangolo.com/) for the API framework +- [FAISS](https://github.com/facebookresearch/faiss) for vector search + +--- + +Made with ❤️ for Adriana James \ No newline at end of file diff --git a/backend/brand_style.py b/backend/brand_style.py new file mode 100644 index 0000000..6232d35 --- /dev/null +++ b/backend/brand_style.py @@ -0,0 +1,175 @@ +""" +Brand style module for the Marketing Assistant AI. +Ensures generated content aligns with Adriana James' brand voice and tone. +""" + +import json +import os +from typing import Dict, List, Any, Optional +from pathlib import Path +from loguru import logger + +import config + +class BrandStyleManager: + """Manages brand style guidelines and ensures content consistency.""" + + def __init__(self): + """Initialize the BrandStyleManager with default or stored style guidelines.""" + self.style_path = Path(config.DATA_DIR) / "style_guidelines" / "brand_style.json" + self.style_guidelines = self._load_or_create_style() + logger.info("BrandStyleManager initialized successfully") + + def _load_or_create_style(self) -> Dict[str, Any]: + """Load existing style guidelines or create new ones with defaults.""" + try: + if self.style_path.exists(): + with open(self.style_path, 'r') as f: + style = json.load(f) + logger.info("Loaded existing brand style guidelines") + return style + else: + # Create directory if it doesn't exist + self.style_path.parent.mkdir(exist_ok=True) + + # Use default style guidelines + style = config.DEFAULT_BRAND_STYLE + + # Save default style + with open(self.style_path, 'w') as f: + json.dump(style, f, indent=2) + + logger.info("Created default brand style guidelines") + return style + except Exception as e: + logger.error(f"Error loading or creating style guidelines: {str(e)}") + # Fall back to default style + return config.DEFAULT_BRAND_STYLE + + def get_style_guidelines(self) -> Dict[str, Any]: + """ + Get current brand style guidelines. + + Returns: + Dictionary of style guidelines + """ + return self.style_guidelines + + def update_style_guidelines(self, new_style: Dict[str, Any]) -> Dict[str, Any]: + """ + Update brand style guidelines. + + Args: + new_style: Dictionary with new style guidelines + + Returns: + Updated style guidelines dictionary + """ + try: + # Merge new style with existing + for key, value in new_style.items(): + self.style_guidelines[key] = value + + # Ensure brand name is preserved + self.style_guidelines['brand_name'] = config.BRAND_NAME + + # Save updated style + with open(self.style_path, 'w') as f: + json.dump(self.style_guidelines, f, indent=2) + + logger.info("Updated brand style guidelines") + return self.style_guidelines + except Exception as e: + logger.error(f"Error updating style guidelines: {str(e)}") + raise + + def format_prompt_with_brand_style(self, user_prompt: str, content_type: Optional[str] = None) -> str: + """ + Format user prompt with brand style guidelines for the LLM. + + Args: + user_prompt: Original user prompt + content_type: Type of content being generated + + Returns: + Formatted prompt with brand style instructions + """ + style = self.style_guidelines + + # Create a formatted prompt with brand style instructions + prompt_parts = [ + f"Generate marketing content for {style['brand_name']} based on the following request:", + f"\"{user_prompt}\"", + "\nFollow these brand style guidelines:", + f"- Brand Name: {style['brand_name']}", + f"- Tone: {', '.join(style.get('tone', []))}", + f"- Voice Characteristics: {', '.join(style.get('voice_characteristics', []))}", + ] + + # Add taboo words if any + if 'taboo_words' in style and style['taboo_words']: + prompt_parts.append(f"- Avoid these words: {', '.join(style['taboo_words'])}") + + # Add preferred terms if any + if 'preferred_terms' in style and style['preferred_terms']: + terms = [f"use '{value}' instead of '{key}'" for key, value in style['preferred_terms'].items()] + prompt_parts.append(f"- Preferred terminology: {'; '.join(terms)}") + + # Add content type specific instructions + if content_type: + if content_type == "email_campaign": + prompt_parts.append("- Format as a professional email with subject line, greeting, body, and signature") + elif content_type == "social_media": + prompt_parts.append("- Format as a concise social media post with appropriate hashtags") + elif content_type == "blog_post": + prompt_parts.append("- Format as a blog post with title, introduction, body with subheadings, and conclusion") + elif content_type == "website_copy": + prompt_parts.append("- Format as website copy with clear headings and concise paragraphs") + elif content_type == "ad_copy": + prompt_parts.append("- Format as advertising copy with headline, body, and clear call to action") + + # Combine all parts + formatted_prompt = "\n".join(prompt_parts) + + logger.debug("Created formatted prompt with brand style") + return formatted_prompt + + def check_content_alignment(self, content: str) -> Dict[str, Any]: + """ + Check if generated content aligns with brand style guidelines. + + Args: + content: Generated marketing content + + Returns: + Dictionary with alignment metrics and suggestions + """ + style = self.style_guidelines + taboo_words = style.get('taboo_words', []) + preferred_terms = style.get('preferred_terms', {}) + + # Check for taboo words + found_taboo_words = [] + for word in taboo_words: + if word.lower() in content.lower(): + found_taboo_words.append(word) + + # Check for preferred terminology + terminology_issues = [] + for avoid, use in preferred_terms.items(): + if avoid.lower() in content.lower(): + terminology_issues.append(f"Found '{avoid}', should use '{use}' instead") + + # Calculate an overall alignment score (simple implementation) + issues_count = len(found_taboo_words) + len(terminology_issues) + alignment_score = max(0, 100 - (issues_count * 10)) # Reduce score for each issue + + return { + 'alignment_score': alignment_score, + 'taboo_words_found': found_taboo_words, + 'terminology_issues': terminology_issues, + 'aligned': alignment_score >= 80 # Consider aligned if score is 80% or higher + } + +# Create a singleton instance +brand_style_manager = BrandStyleManager() \ No newline at end of file diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..214da1a --- /dev/null +++ b/backend/config.py @@ -0,0 +1,87 @@ +""" +Configuration module for the Marketing Assistant AI. +Handles environment variables and application settings. +""" + +import os +from pathlib import Path +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# Base paths +BASE_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = BASE_DIR / "data" + +# Ensure data directories exist +(DATA_DIR / "past_campaigns").mkdir(exist_ok=True) +(DATA_DIR / "user_queries").mkdir(exist_ok=True) +(DATA_DIR / "style_guidelines").mkdir(exist_ok=True) + +# API configuration +API_HOST = os.getenv("API_HOST", "localhost") +API_PORT = int(os.getenv("API_PORT", 8000)) + +# LLM configuration +LLM_MODEL = os.getenv("LLM_MODEL") +LLM_API_KEY = os.getenv("LLM_API_KEY") + +# Cohere configuration +COHERE_API_KEY = os.getenv("COHERE_API_KEY") + +# Vector database configuration +VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", str(DATA_DIR / "vector_store")) + +# Brand configuration +BRAND_NAME = os.getenv("BRAND_NAME", "Adriana James") + +# Content types +CONTENT_TYPES = [ + "email_campaign", + "social_media", + "blog_post", + "website_copy", + "ad_copy", + "funnel_page", + "product_description", + "press_release" +] + +# Tone options +TONE_OPTIONS = [ + "professional", + "friendly", + "excited", + "authoritative", + "casual", + "inspirational", + "empathetic", + "humorous" +] + +# Content length options +LENGTH_OPTIONS = [ + "short", # < 100 words + "medium", # 100-300 words + "long", # > 300 words +] + +# Default brand style guidelines +DEFAULT_BRAND_STYLE = { + "brand_name": BRAND_NAME, + "tone": ["professional", "friendly", "inspirational"], + "voice_characteristics": ["clear", "direct", "empowering"], + "taboo_words": ["cheap", "discount", "bargain"], + "preferred_terms": { + "customers": "clients", + "products": "solutions" + } +} + +# Logging configuration +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") +LOG_FILE = os.getenv("LOG_FILE", str(BASE_DIR / "logs" / "app.log")) + +# Create logs directory if it doesn't exist +(BASE_DIR / "logs").mkdir(exist_ok=True) \ No newline at end of file diff --git a/backend/copywriter.py b/backend/copywriter.py new file mode 100644 index 0000000..ede1bbc --- /dev/null +++ b/backend/copywriter.py @@ -0,0 +1,278 @@ +""" +Copywriter module for the Marketing Assistant AI. +Core AI-powered content generation using a fine-tuned LLM. +""" + +import os +import json +import httpx +from typing import Dict, List, Any, Optional, Tuple +from loguru import logger +from tenacity import retry, stop_after_attempt, wait_exponential + +import config +from brand_style import brand_style_manager +from vector_store import vector_store + +class Copywriter: + """Generates marketing copy using a fine-tuned LLM.""" + + def __init__(self): + """Initialize the Copywriter with Cohere LLM client.""" + self.model = "command" # Cohere's generation model + self.api_key = config.COHERE_API_KEY + logger.info("Copywriter initialized with Cohere API successfully") + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def generate_copy( + self, + prompt: str, + content_type: Optional[str] = None, + tone: Optional[str] = None, + length: Optional[str] = None, + include_cta: bool = False, + reference_similar_content: bool = True, + max_tokens: int = 1000 + ) -> Dict[str, Any]: + """ + Generate marketing copy based on the user prompt and parameters. + + Args: + prompt: User prompt for content generation + content_type: Type of content to generate + tone: Desired tone of the content + length: Desired length of the content + include_cta: Whether to include a call to action + reference_similar_content: Whether to fetch and reference similar content + max_tokens: Maximum tokens for the generated response + + Returns: + Dictionary with generated content and metadata + """ + try: + # Step 1: Format prompt with brand style guidelines + branded_prompt = brand_style_manager.format_prompt_with_brand_style(prompt, content_type) + + # Step 2: Find similar content for reference (if enabled) + reference_content = [] + if reference_similar_content: + search_results = await vector_store.search(prompt, top_k=3) + if search_results: + reference_content = [result['text'] for result in search_results] + + # Step 3: Add additional instructions based on parameters + full_prompt = branded_prompt + + if tone: + full_prompt += f"\n- Use a {tone} tone" + + if length: + length_instructions = { + "short": "Keep the content brief and to the point (under 100 words).", + "medium": "Write a moderate amount of content (100-300 words).", + "long": "Create comprehensive content with depth (over 300 words)." + } + full_prompt += f"\n- {length_instructions.get(length, '')}" + + if include_cta: + full_prompt += "\n- Include a strong call to action at the end" + + # Step 4: Add reference content if available + if reference_content: + full_prompt += "\n\nFor reference, here are some similar pieces of content that have performed well in the past:" + for i, content in enumerate(reference_content, 1): + # Truncate reference content if it's too long + preview = content[:300] + "..." if len(content) > 300 else content + full_prompt += f"\n\nReference {i}:\n{preview}" + + full_prompt += "\n\nUse these references for inspiration, but create original content." + + # Step 5: Generate content using the LLM + generated_content = await self._call_llm_api(full_prompt, max_tokens) + + # Step 6: Check content alignment with brand style + alignment_check = brand_style_manager.check_content_alignment(generated_content) + + # Step 7: Generate alternative headline suggestions + headline_suggestions = await self._generate_headline_suggestions(prompt, generated_content) + + # Step 8: Return the generated content with metadata + result = { + "content": generated_content, + "suggestions": headline_suggestions, + "metadata": { + "content_type": content_type, + "tone": tone, + "alignment_score": alignment_check['alignment_score'], + "generated_at": None # Will be added by the API + } + } + + # Add alignment issues if any + if alignment_check['taboo_words_found'] or alignment_check['terminology_issues']: + result["alignment_issues"] = { + "taboo_words_found": alignment_check['taboo_words_found'], + "terminology_issues": alignment_check['terminology_issues'] + } + + logger.info(f"Generated content with {len(generated_content)} characters") + return result + + except Exception as e: + logger.error(f"Error generating copy: {str(e)}") + raise + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def _call_llm_api(self, prompt: str, max_tokens: int = 1000) -> str: + """ + Call the Cohere API to generate content. + + Args: + prompt: The formatted prompt for the LLM + max_tokens: Maximum tokens for the generated response + + Returns: + Generated content as a string + """ + try: + # Use Cohere's generate API with the API key from config + cohere_api_key = config.COHERE_API_KEY + + async with httpx.AsyncClient() as client: + response = await client.post( + "https://api.cohere.ai/v1/generate", + headers={ + "Authorization": f"Bearer {cohere_api_key}", + "Content-Type": "application/json" + }, + json={ + "model": "command", # Cohere's generation model + "prompt": prompt, + "max_tokens": max_tokens, + "temperature": 0.7, + "k": 0, + "p": 0.75 + }, + timeout=30.0 + ) + + if response.status_code == 200: + result = response.json() + return result["generations"][0]["text"].strip() + else: + logger.error(f"Cohere API error: {response.status_code}, {response.text}") + raise Exception(f"Cohere API error: {response.status_code}") + + except Exception as e: + logger.error(f"Error calling Cohere API: {str(e)}") + raise + + async def _generate_headline_suggestions(self, original_prompt: str, generated_content: str) -> List[str]: + """ + Generate alternative headline suggestions based on the content. + + Args: + original_prompt: The original user prompt + generated_content: The generated marketing content + + Returns: + List of headline suggestions + """ + try: + # This would call the LLM to generate headlines + # Simplified mock response for demonstration + return [ + "Alternative Headline 1: Discover the Power of Adriana James' Solutions", + "Alternative Headline 2: Transform Your Results with Adriana James", + "Alternative Headline 3: The Adriana James Approach: Excellence Redefined" + ] + except Exception as e: + logger.error(f"Error generating headline suggestions: {str(e)}") + return [] + + async def improve_copy(self, content: str, feedback: str) -> str: + """ + Improve content based on user feedback. + + Args: + content: Original generated content + feedback: User feedback for improvement + + Returns: + Improved content + """ + try: + # Format prompt for improvement + improve_prompt = f""" + Please improve the following marketing content based on the feedback provided: + + ORIGINAL CONTENT: + {content} + + FEEDBACK: + {feedback} + + IMPROVED CONTENT: + """ + + # Call LLM to improve content + improved_content = await self._call_llm_api(improve_prompt, max_tokens=1200) + + logger.info(f"Improved content based on feedback") + return improved_content + + except Exception as e: + logger.error(f"Error improving content: {str(e)}") + raise + + async def analyze_content_performance(self, content: str) -> Dict[str, Any]: + """ + Analyze marketing content for performance prediction. + + Args: + content: Marketing content to analyze + + Returns: + Dictionary with analysis results + """ + try: + # This would be enhanced with actual ML models in production + # Simplified mock response for demonstration + + # Very basic analysis using length and keyword presence + word_count = len(content.split()) + has_cta = any(phrase in content.lower() for phrase in ["call", "contact", "get started", "try", "buy", "sign up"]) + sentence_count = len([s for s in content.split(".") if s.strip()]) + avg_words_per_sentence = word_count / max(1, sentence_count) + + # Simple scoring system + readability_score = 100 - min(100, max(0, abs(avg_words_per_sentence - 15) * 5)) + cta_score = 90 if has_cta else 60 + length_score = min(100, max(0, word_count / 3)) + + overall_score = (readability_score + cta_score + length_score) / 3 + + return { + "overall_score": round(overall_score, 1), + "readability_score": round(readability_score, 1), + "cta_effectiveness": round(cta_score, 1), + "length_appropriateness": round(length_score, 1), + "metrics": { + "word_count": word_count, + "sentence_count": sentence_count, + "avg_words_per_sentence": round(avg_words_per_sentence, 1), + "has_cta": has_cta + }, + "improvement_suggestions": [ + "Consider adding a stronger call to action" if cta_score < 80 else "Your call to action is effective", + "Try to use shorter sentences for better readability" if avg_words_per_sentence > 20 else "Your sentence length is good for readability", + "Consider adding more content for better engagement" if word_count < 100 else "Your content length is appropriate" + ] + } + + except Exception as e: + logger.error(f"Error analyzing content: {str(e)}") + raise + +# Create a singleton instance +copywriter = Copywriter() \ No newline at end of file diff --git a/backend/data/vector_store/faiss_index.bin b/backend/data/vector_store/faiss_index.bin new file mode 100644 index 0000000..969a818 Binary files /dev/null and b/backend/data/vector_store/faiss_index.bin differ diff --git a/backend/data/vector_store/metadata.pkl b/backend/data/vector_store/metadata.pkl new file mode 100644 index 0000000..6d6ae08 Binary files /dev/null and b/backend/data/vector_store/metadata.pkl differ diff --git a/backend/embeddings.py b/backend/embeddings.py new file mode 100644 index 0000000..27b47c2 --- /dev/null +++ b/backend/embeddings.py @@ -0,0 +1,138 @@ +""" +Embeddings module for the Marketing Assistant AI. +Uses Cohere to generate and manage text embeddings. +""" + +import cohere +from typing import List, Dict, Any, Optional +import numpy as np +from loguru import logger +from tenacity import retry, stop_after_attempt, wait_exponential + +import config + +class EmbeddingsManager: + """Manages the generation and manipulation of text embeddings using Cohere.""" + + def __init__(self): + """Initialize the EmbeddingsManager with Cohere API client.""" + try: + self.co = cohere.Client(config.COHERE_API_KEY) + logger.info("EmbeddingsManager initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize EmbeddingsManager: {str(e)}") + raise + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def get_embeddings(self, texts: List[str], model: str = "embed-english-v3.0") -> np.ndarray: + """ + Generate embeddings for a list of texts. + + Args: + texts: List of text strings to embed + model: Cohere embedding model to use + + Returns: + numpy.ndarray: Array of embeddings vectors + """ + try: + if not texts: + logger.warning("Empty text list provided for embedding") + return np.array([]) + + # Ensure texts are not too long for the API + processed_texts = [text[:8192] for text in texts] + + response = self.co.embed( + texts=processed_texts, + model=model, + input_type="search_document" + ) + + embeddings = np.array(response.embeddings) + logger.debug(f"Generated {len(embeddings)} embeddings with shape {embeddings.shape}") + return embeddings + + except Exception as e: + logger.error(f"Error generating embeddings: {str(e)}") + raise + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def get_query_embedding(self, text: str, model: str = "embed-english-v3.0") -> np.ndarray: + """ + Generate embedding for a single query text. + + Args: + text: The query text to embed + model: Cohere embedding model to use + + Returns: + numpy.ndarray: Embedding vector for the query + """ + try: + response = self.co.embed( + texts=[text[:8192]], + model=model, + input_type="search_query" + ) + + embedding = np.array(response.embeddings[0]) + return embedding + + except Exception as e: + logger.error(f"Error generating query embedding: {str(e)}") + raise + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def rerank_results( + self, + query: str, + documents: List[str], + model: str = "rerank-english-v2.0", + top_n: int = 5 + ) -> List[Dict[str, Any]]: + """ + Rerank documents based on relevance to the query. + + Args: + query: The search query + documents: List of documents to rerank + model: Cohere reranking model to use + top_n: Number of top results to return + + Returns: + List of dictionaries with document index and relevance score + """ + try: + if not documents: + logger.warning("Empty document list provided for reranking") + return [] + + # Truncate documents if they're too long + processed_docs = [doc[:8192] for doc in documents] + + response = self.co.rerank( + query=query, + documents=processed_docs, + model=model, + top_n=min(top_n, len(processed_docs)) + ) + + results = [ + { + "index": result.index, + "document": documents[result.index], + "relevance_score": result.relevance_score + } + for result in response.results + ] + + logger.debug(f"Reranked {len(documents)} documents, returning top {len(results)}") + return results + + except Exception as e: + logger.error(f"Error reranking documents: {str(e)}") + raise + +# Create a singleton instance +embeddings_manager = EmbeddingsManager() \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..673aca0 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,406 @@ +""" +Main FastAPI application for the Marketing Assistant AI. +Provides API endpoints for generating and managing marketing content. +""" + +import os +import json +from typing import Dict, List, Any, Optional +from datetime import datetime +from pathlib import Path +from fastapi import FastAPI, HTTPException, Depends, Query, Body, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from loguru import logger +from pydantic import BaseModel, Field + +import config +from copywriter import copywriter +from vector_store import vector_store +from brand_style import brand_style_manager +from embeddings import embeddings_manager + +# Initialize logging +logger.add(config.LOG_FILE, level=config.LOG_LEVEL, rotation="10 MB", retention="1 month") + +# Create FastAPI app +app = FastAPI( + title="Marketing Assistant AI", + description="AI-powered tool for marketing copywriting with Adriana James' brand voice", + version="1.0.0" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify your frontend domain + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Define request and response models +class GenerateCopyRequest(BaseModel): + prompt: str = Field(..., description="The main instruction for generating content") + content_type: Optional[str] = Field(None, description="Type of content to generate") + tone: Optional[str] = Field(None, description="Desired tone of the content") + length: Optional[str] = Field(None, description="Desired length of the content") + include_cta: Optional[bool] = Field(False, description="Whether to include a call to action") + reference_similar_content: Optional[bool] = Field(True, description="Whether to reference similar content") + max_tokens: Optional[int] = Field(1000, description="Maximum tokens for the generated response") + +class TrainingDataRequest(BaseModel): + content_type: str = Field(..., description="Type of content") + content: str = Field(..., description="The marketing content") + metadata: Optional[Dict[str, Any]] = Field({}, description="Additional metadata about the content") + +class BrandStyleUpdateRequest(BaseModel): + tone: Optional[List[str]] = Field(None, description="Brand tone options") + voice_characteristics: Optional[List[str]] = Field(None, description="Voice characteristics") + taboo_words: Optional[List[str]] = Field(None, description="Words to avoid") + preferred_terms: Optional[Dict[str, str]] = Field(None, description="Preferred terminology") + +class ContentImprovementRequest(BaseModel): + content: str = Field(..., description="Original generated content") + feedback: str = Field(..., description="User feedback for improvement") + +# API Routes +@app.get("/") +async def root(): + """Root endpoint with API information.""" + return { + "name": "Marketing Assistant AI", + "version": "1.0.0", + "description": f"AI-powered marketing copywriter for {config.BRAND_NAME}" + } + +@app.post("/generate-copy") +async def generate_copy(request: GenerateCopyRequest): + """Generate marketing copy based on the provided prompt and parameters.""" + try: + # Validate content type if provided + if request.content_type and request.content_type not in config.CONTENT_TYPES: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "status": "error", + "message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}" + } + ) + + # Validate tone if provided + if request.tone and request.tone not in config.TONE_OPTIONS: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "status": "error", + "message": f"Invalid tone. Must be one of: {', '.join(config.TONE_OPTIONS)}" + } + ) + + # Validate length if provided + if request.length and request.length not in config.LENGTH_OPTIONS: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "status": "error", + "message": f"Invalid length. Must be one of: {', '.join(config.LENGTH_OPTIONS)}" + } + ) + + # Generate copy + result = await copywriter.generate_copy( + prompt=request.prompt, + content_type=request.content_type, + tone=request.tone, + length=request.length, + include_cta=request.include_cta, + reference_similar_content=request.reference_similar_content, + max_tokens=request.max_tokens + ) + + # Add timestamp + result["metadata"]["generated_at"] = datetime.now().isoformat() + + # Store the generated content in the vector store for future reference + if result["content"]: + metadata = { + "content_type": request.content_type, + "tone": request.tone, + "prompt": request.prompt, + "generated": True + } + await vector_store.add_documents([result["content"]], [metadata]) + + # Store the user query for future training + query_path = Path(config.DATA_DIR) / "user_queries" / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.json" + with open(query_path, 'w') as f: + json.dump({ + "prompt": request.prompt, + "parameters": { + "content_type": request.content_type, + "tone": request.tone, + "length": request.length, + "include_cta": request.include_cta + }, + "timestamp": datetime.now().isoformat() + }, f, indent=2) + + return { + "status": "success", + "content": result["content"], + "suggestions": result.get("suggestions", []), + "metadata": result["metadata"] + } + + except Exception as e: + logger.error(f"Error generating copy: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to generate copy: {str(e)}" + ) + +@app.get("/brand-style") +async def get_brand_style(): + """Get the current brand style guidelines.""" + try: + style = brand_style_manager.get_style_guidelines() + return style + except Exception as e: + logger.error(f"Error getting brand style: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get brand style: {str(e)}" + ) + +@app.put("/brand-style") +async def update_brand_style(request: BrandStyleUpdateRequest): + """Update the brand style guidelines.""" + try: + update_data = request.dict(exclude_unset=True) + updated_style = brand_style_manager.update_style_guidelines(update_data) + + return { + "status": "success", + "message": "Brand style updated successfully", + "style": updated_style + } + except Exception as e: + logger.error(f"Error updating brand style: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update brand style: {str(e)}" + ) + +@app.post("/training-data") +async def add_training_data(request: TrainingDataRequest): + """Add new marketing content for AI training.""" + try: + # Validate content type + if request.content_type not in config.CONTENT_TYPES: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "status": "error", + "message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}" + } + ) + + # Add metadata + metadata = request.metadata.copy() + metadata["content_type"] = request.content_type + metadata["added_at"] = datetime.now().isoformat() + metadata["training_data"] = True + + # Add to vector store + doc_ids = await vector_store.add_documents([request.content], [metadata]) + + # Save to past campaigns + campaign_path = Path(config.DATA_DIR) / "past_campaigns" / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.json" + with open(campaign_path, 'w') as f: + json.dump({ + "content": request.content, + "content_type": request.content_type, + "metadata": metadata, + "document_id": doc_ids[0] if doc_ids else None, + "timestamp": datetime.now().isoformat() + }, f, indent=2) + + return { + "status": "success", + "message": "Training data added successfully", + "data_id": doc_ids[0] if doc_ids else None + } + except Exception as e: + logger.error(f"Error adding training data: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to add training data: {str(e)}" + ) + +@app.get("/training-data") +async def list_training_data( + content_type: Optional[str] = Query(None, description="Filter by content type"), + page: int = Query(1, ge=1, description="Page number"), + limit: int = Query(10, ge=1, le=100, description="Items per page") +): + """Retrieve a list of available training data.""" + try: + # Build filters + filters = {} + if content_type: + if content_type not in config.CONTENT_TYPES: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "status": "error", + "message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}" + } + ) + filters["content_type"] = content_type + + filters["training_data"] = True + + # Fetch all matching documents first (not efficient for large datasets but works for demo) + all_docs = [] + for i in range(len(vector_store.metadata)): + doc = await vector_store.get_document(i) + if doc and all(doc["metadata"].get(k) == v for k, v in filters.items()): + all_docs.append(doc) + + # Sort by timestamp (newest first) + all_docs.sort(key=lambda x: x["metadata"].get("added_at", ""), reverse=True) + + # Paginate + total = len(all_docs) + pages = (total + limit - 1) // limit if total > 0 else 1 + start = (page - 1) * limit + end = start + limit + paginated_docs = all_docs[start:end] + + # Format the response + items = [] + for doc in paginated_docs: + # Get a preview of the text (first 100 characters) + preview = doc["text"][:100] + "..." if len(doc["text"]) > 100 else doc["text"] + + items.append({ + "id": doc["document_id"], + "content_type": doc["metadata"].get("content_type", "unknown"), + "preview": preview, + "added_at": doc["metadata"].get("added_at", "") + }) + + return { + "items": items, + "pagination": { + "total": total, + "page": page, + "limit": limit, + "pages": pages + } + } + except Exception as e: + logger.error(f"Error listing training data: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to list training data: {str(e)}" + ) + +@app.get("/training-data/{document_id}") +async def get_training_data(document_id: int): + """Retrieve a specific training document by ID.""" + try: + doc = await vector_store.get_document(document_id) + if not doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Document with ID {document_id} not found" + ) + + return { + "id": doc["document_id"], + "content": doc["text"], + "metadata": doc["metadata"] + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Error retrieving training data: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve training data: {str(e)}" + ) + +@app.delete("/training-data/{document_id}") +async def delete_training_data(document_id: int): + """Delete a specific training document by ID.""" + try: + success = await vector_store.delete_document(document_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Document with ID {document_id} not found or could not be deleted" + ) + + return { + "status": "success", + "message": f"Document with ID {document_id} successfully deleted" + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting training data: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete training data: {str(e)}" + ) + +@app.post("/improve-content") +async def improve_content(request: ContentImprovementRequest): + """Improve content based on user feedback.""" + try: + improved_content = await copywriter.improve_copy( + content=request.content, + feedback=request.feedback + ) + + return { + "status": "success", + "original_content": request.content, + "improved_content": improved_content, + "feedback": request.feedback + } + except Exception as e: + logger.error(f"Error improving content: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to improve content: {str(e)}" + ) + +@app.post("/analyze-content") +async def analyze_content(content: str = Body(..., embed=True)): + """Analyze marketing content for performance prediction.""" + try: + analysis = await copywriter.analyze_content_performance(content) + + return { + "status": "success", + "analysis": analysis + } + except Exception as e: + logger.error(f"Error analyzing content: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to analyze content: {str(e)}" + ) + +# Run the application +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host=config.API_HOST, + port=config.API_PORT, + reload=True + ) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..37aa443 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,15 @@ +fastapi +uvicorn +pydantic +python-dotenv +httpx +faiss-cpu +numpy==1.26.2 +pandas +cohere +python-multipart +SQLAlchemy +databases +aiosqlite +loguru +tenacity \ No newline at end of file diff --git a/backend/vector_store.py b/backend/vector_store.py new file mode 100644 index 0000000..7ab68d9 --- /dev/null +++ b/backend/vector_store.py @@ -0,0 +1,347 @@ +""" +Vector store module for the Marketing Assistant AI. +Uses FAISS for efficient storage and retrieval of content embeddings. +""" + +import os +import json +import pickle +import faiss +import numpy as np +from typing import List, Dict, Any, Optional, Tuple +from pathlib import Path +from loguru import logger +from datetime import datetime + +import config +from embeddings import embeddings_manager + +class VectorStore: + """Manages vector database operations for content retrieval.""" + + def __init__(self): + """Initialize the VectorStore with FAISS index.""" + self.store_path = Path(config.VECTOR_DB_PATH) + self.store_path.mkdir(exist_ok=True) + + self.index_path = self.store_path / "faiss_index.bin" + self.metadata_path = self.store_path / "metadata.pkl" + + self.dimension = None + self.index = None + self.metadata = [] + + self._load_or_create_index() + logger.info("VectorStore initialized successfully") + + def _load_or_create_index(self) -> None: + """Load existing index or create new one if it doesn't exist.""" + try: + if self.index_path.exists() and self.metadata_path.exists(): + # Load existing index and metadata + self.index = faiss.read_index(str(self.index_path)) + with open(self.metadata_path, 'rb') as f: + self.metadata = pickle.load(f) + self.dimension = self.index.d + logger.info(f"Loaded existing vector index with {self.index.ntotal} vectors") + else: + # Default dimension for Cohere embeddings + self.dimension = 1024 + self.index = faiss.IndexFlatL2(self.dimension) + self.metadata = [] + logger.info(f"Created new vector index with dimension {self.dimension}") + + # Save the empty index and metadata + self._save_index() + except Exception as e: + logger.error(f"Error loading or creating index: {str(e)}") + raise + + def _save_index(self) -> None: + """Save the index and metadata to disk.""" + try: + faiss.write_index(self.index, str(self.index_path)) + with open(self.metadata_path, 'wb') as f: + pickle.dump(self.metadata, f) + logger.debug("Saved vector index and metadata") + except Exception as e: + logger.error(f"Error saving index: {str(e)}") + raise + + async def add_documents( + self, + texts: List[str], + metadata_list: Optional[List[Dict[str, Any]]] = None + ) -> List[int]: + """ + Add documents to the vector store. + + Args: + texts: List of text documents to add + metadata_list: List of metadata dictionaries for each document + + Returns: + List of document IDs (vector indices) + """ + try: + if not texts: + logger.warning("No texts provided to add to vector store") + return [] + + if metadata_list is None: + metadata_list = [{} for _ in texts] + + if len(texts) != len(metadata_list): + raise ValueError("Number of texts and metadata entries must match") + + # Generate embeddings + embeddings = await embeddings_manager.get_embeddings(texts) + + # Check if embeddings match our dimension + if embeddings.shape[1] != self.dimension: + logger.warning(f"Embedding dimension mismatch: expected {self.dimension}, got {embeddings.shape[1]}") + # If we have no documents yet, we can adapt to the new dimension + if self.index.ntotal == 0: + self.dimension = embeddings.shape[1] + self.index = faiss.IndexFlatL2(self.dimension) + logger.info(f"Adapted to new dimension: {self.dimension}") + else: + raise ValueError(f"Embedding dimension mismatch: expected {self.dimension}, got {embeddings.shape[1]}") + + # Add timestamp to metadata + timestamp = datetime.now().isoformat() + for meta in metadata_list: + meta['timestamp'] = timestamp + meta['document_id'] = len(self.metadata) + len(metadata_list) + + # Store texts in metadata + for i, (text, meta) in enumerate(zip(texts, metadata_list)): + meta['text'] = text + + # Add vectors to index + start_idx = self.index.ntotal + self.index.add(embeddings.astype(np.float32)) + self.metadata.extend(metadata_list) + + # Save updated index + self._save_index() + + # Return document IDs + doc_ids = list(range(start_idx, start_idx + len(texts))) + logger.info(f"Added {len(texts)} documents to vector store") + return doc_ids + + except Exception as e: + logger.error(f"Error adding documents to vector store: {str(e)}") + raise + + async def search( + self, + query: str, + top_k: int = 5, + filters: Optional[Dict[str, Any]] = None, + rerank: bool = True + ) -> List[Dict[str, Any]]: + """ + Search for similar documents. + + Args: + query: The search query + top_k: Number of results to return + filters: Dictionary of metadata filters + rerank: Whether to use Cohere's reranking + + Returns: + List of result dictionaries with document content and metadata + """ + try: + if self.index.ntotal == 0: + logger.warning("Empty vector store, no results to return") + return [] + + # Generate query embedding + query_embedding = await embeddings_manager.get_query_embedding(query) + query_embedding = query_embedding.reshape(1, -1).astype(np.float32) + + # First pass: find more candidates than needed for reranking + search_k = top_k * 3 if rerank else top_k + search_k = min(search_k, self.index.ntotal) # Don't request more than we have + + distances, indices = self.index.search(query_embedding, search_k) + + # Get metadata and texts for matching indices + results = [] + for i, idx in enumerate(indices[0]): + if idx < 0 or idx >= len(self.metadata): + continue # Skip invalid indices + + metadata = self.metadata[idx] + text = metadata.get('text', '') + + # Apply filters if any + if filters and not self._matches_filters(metadata, filters): + continue + + results.append({ + 'document_id': idx, + 'text': text, + 'metadata': {k: v for k, v in metadata.items() if k != 'text'}, + 'distance': float(distances[0][i]) + }) + + # Apply reranking if requested + if rerank and results: + texts = [r['text'] for r in results] + reranked = await embeddings_manager.rerank_results(query, texts, top_n=top_k) + + # Map reranked results back to our original results + reranked_results = [] + for item in reranked: + orig_idx = item['index'] + if 0 <= orig_idx < len(results): + reranked_results.append({ + **results[orig_idx], + 'relevance_score': item['relevance_score'] + }) + + results = reranked_results + else: + # Just take the top_k results + results = results[:top_k] + + logger.info(f"Found {len(results)} matching documents for query") + return results + + except Exception as e: + logger.error(f"Error searching vector store: {str(e)}") + raise + + def _matches_filters(self, metadata: Dict[str, Any], filters: Dict[str, Any]) -> bool: + """Check if metadata matches the specified filters.""" + for key, value in filters.items(): + if key not in metadata: + return False + + if isinstance(value, list): + # Check if metadata value is in the list + if metadata[key] not in value: + return False + elif metadata[key] != value: + return False + + return True + + async def delete_document(self, document_id: int) -> bool: + """ + Delete a document from the vector store. + + Args: + document_id: ID of the document to delete + + Returns: + Boolean indicating success + """ + try: + if document_id < 0 or document_id >= len(self.metadata): + logger.warning(f"Invalid document ID: {document_id}") + return False + + # FAISS doesn't support direct deletion, so we need to rebuild the index + # Mark the document as deleted in metadata + self.metadata[document_id]['deleted'] = True + + # Save updated metadata + self._save_index() + + logger.info(f"Marked document {document_id} as deleted") + return True + + except Exception as e: + logger.error(f"Error deleting document: {str(e)}") + raise + + async def get_document(self, document_id: int) -> Optional[Dict[str, Any]]: + """ + Retrieve a document by ID. + + Args: + document_id: ID of the document to retrieve + + Returns: + Document with metadata or None if not found + """ + try: + if document_id < 0 or document_id >= len(self.metadata): + logger.warning(f"Invalid document ID: {document_id}") + return None + + metadata = self.metadata[document_id] + + # Check if document is marked as deleted + if metadata.get('deleted', False): + logger.warning(f"Document {document_id} is marked as deleted") + return None + + text = metadata.get('text', '') + + return { + 'document_id': document_id, + 'text': text, + 'metadata': {k: v for k, v in metadata.items() if k != 'text' and k != 'deleted'} + } + + except Exception as e: + logger.error(f"Error retrieving document: {str(e)}") + raise + + async def update_document(self, document_id: int, text: str, metadata: Optional[Dict[str, Any]] = None) -> bool: + """ + Update a document in the vector store. + + Args: + document_id: ID of the document to update + text: New document text + metadata: New metadata (will be merged with existing) + + Returns: + Boolean indicating success + """ + try: + if document_id < 0 or document_id >= len(self.metadata): + logger.warning(f"Invalid document ID: {document_id}") + return False + + # Get existing metadata + existing_metadata = self.metadata[document_id] + + # Check if document is marked as deleted + if existing_metadata.get('deleted', False): + logger.warning(f"Cannot update deleted document {document_id}") + return False + + # Generate new embedding + embeddings = await embeddings_manager.get_embeddings([text]) + + # Update the vector in the index + faiss.IndexFlatL2_update_vectors(self.index, embeddings.astype(np.float32), np.array([document_id], dtype=np.int64)) + + # Update metadata + if metadata: + for key, value in metadata.items(): + existing_metadata[key] = value + + existing_metadata['text'] = text + existing_metadata['updated_at'] = datetime.now().isoformat() + + # Save updated index + self._save_index() + + logger.info(f"Updated document {document_id}") + return True + + except Exception as e: + logger.error(f"Error updating document: {str(e)}") + raise + +# Create a singleton instance +vector_store = VectorStore() \ No newline at end of file diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..4b8bea4 --- /dev/null +++ b/data/README.md @@ -0,0 +1,81 @@ +# Marketing Assistant AI - Data Directory + +This directory contains the data used by the Marketing Assistant AI system. + +## Structure + +- **past_campaigns/**: Contains JSON files of past marketing campaigns used for training and reference +- **user_queries/**: Stores user queries and requests for analytics and model improvement +- **style_guidelines/**: Contains brand tone and voice guidelines +- **vector_store/**: Generated vector database for content retrieval (created automatically) + +## File Formats + +### Past Campaigns + +Past campaign files are stored as JSON with the following structure: + +```json +{ + "content": "The actual marketing content text", + "content_type": "email_campaign|social_media|blog_post|etc", + "metadata": { + "campaign_name": "Name of the campaign", + "performance_metrics": { + "metric1": value, + "metric2": value + }, + "content_type": "Same as above", + "added_at": "ISO timestamp", + "training_data": true + }, + "document_id": 0, + "timestamp": "ISO timestamp" +} +``` + +### User Queries + +User query files store information about requests made to the AI: + +```json +{ + "prompt": "The user's prompt text", + "parameters": { + "content_type": "Type of content requested", + "tone": "Requested tone", + "length": "Requested length", + "include_cta": true|false + }, + "timestamp": "ISO timestamp" +} +``` + +### Brand Style Guidelines + +Brand style is stored as a JSON file with the following structure: + +```json +{ + "brand_name": "Adriana James", + "tone": ["professional", "friendly", "inspirational"], + "voice_characteristics": ["clear", "direct", "empowering"], + "taboo_words": ["cheap", "discount", "bargain"], + "preferred_terms": { + "customers": "clients", + "products": "solutions" + } +} +``` + +## Adding New Data + +### Adding Past Campaigns + +1. Use the API endpoint `POST /training-data` with the appropriate JSON payload +2. Alternatively, add a JSON file to the `past_campaigns` directory following the format above + +### Updating Brand Style + +1. Use the API endpoint `PUT /brand-style` with the updated style guidelines +2. The system will automatically update the style file \ No newline at end of file diff --git a/data/past_campaigns/.gitkeep b/data/past_campaigns/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/style_guidelines/brand_style.json b/data/style_guidelines/brand_style.json new file mode 100644 index 0000000..43e5c62 --- /dev/null +++ b/data/style_guidelines/brand_style.json @@ -0,0 +1,36 @@ +{ + "brand_name": "Adriana James", + "tone": [ + "professional", + "friendly", + "inspirational", + "empowering", + "excited", + "authoritative" + ], + "voice_characteristics": [ + "clear", + "direct", + "empowering", + "confident", + "authentic", + "innovative", + "visionary", + "approachable" + ], + "taboo_words": [ + "cheap", + "discount", + "bargain", + "failure", + "impossible", + "difficult" + ], + "preferred_terms": { + "customers": "clients", + "products": "solutions", + "problems": "challenges", + "services": "experiences", + "training": "transformation" + } +} \ No newline at end of file diff --git a/data/user_queries/.gitkeep b/data/user_queries/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/API_DOCUMENTATION.md b/docs/API_DOCUMENTATION.md new file mode 100644 index 0000000..e9dbdf1 --- /dev/null +++ b/docs/API_DOCUMENTATION.md @@ -0,0 +1,187 @@ +# Marketing Assistant AI - API Documentation + +## API Endpoints + +### Generate Copy + +Generates marketing copy based on the provided prompt and optional parameters. + +**Endpoint**: `/generate-copy` +**Method**: POST +**Content-Type**: application/json + +**Request Body**: +```json +{ + "prompt": "Write a social media post for our new product launch", + "content_type": "social_media", + "tone": "excited", + "length": "medium", + "include_cta": true +} +``` + +**Parameters**: +- `prompt` (string, required): The main instruction for generating content +- `content_type` (string, optional): Type of content to generate (social_media, email, blog, website, etc.) +- `tone` (string, optional): Desired tone (excited, professional, casual, etc.) +- `length` (string, optional): Content length (short, medium, long) +- `include_cta` (boolean, optional): Whether to include a call to action + +**Response**: +```json +{ + "status": "success", + "content": "Exciting news! Our revolutionary new product has just landed...", + "suggestions": [ + "Alternative headline option 1", + "Alternative headline option 2" + ], + "metadata": { + "content_type": "social_media", + "tone": "excited", + "word_count": 85, + "generated_at": "2025-04-17T10:30:45Z" + } +} +``` + +### Get Brand Style Guidelines + +Retrieves the current brand style guidelines. + +**Endpoint**: `/brand-style` +**Method**: GET + +**Response**: +```json +{ + "brand_name": "Adriana James", + "tone": ["professional", "friendly", "inspiring"], + "voice_characteristics": ["clear", "direct", "empowering"], + "taboo_words": ["cheap", "discount", "bargain"], + "preferred_terms": { + "customers": "clients", + "products": "solutions" + } +} +``` + +### Update Brand Style + +Updates the brand style guidelines. + +**Endpoint**: `/brand-style` +**Method**: PUT +**Content-Type**: application/json + +**Request Body**: +```json +{ + "tone": ["professional", "friendly", "inspiring", "innovative"], + "voice_characteristics": ["clear", "direct", "empowering"], + "taboo_words": ["cheap", "discount", "bargain", "basic"], + "preferred_terms": { + "customers": "clients", + "products": "solutions", + "problems": "challenges" + } +} +``` + +**Response**: +```json +{ + "status": "success", + "message": "Brand style updated successfully" +} +``` + +### Add Training Data + +Adds new marketing content for AI training. + +**Endpoint**: `/training-data` +**Method**: POST +**Content-Type**: application/json + +**Request Body**: +```json +{ + "content_type": "email_campaign", + "content": "Dear valued client, We're thrilled to announce...", + "metadata": { + "campaign_name": "Spring Launch 2025", + "performance_metrics": { + "open_rate": 0.42, + "click_rate": 0.15 + } + } +} +``` + +**Response**: +```json +{ + "status": "success", + "message": "Training data added successfully", + "data_id": "12345" +} +``` + +### List Training Data + +Retrieves a list of available training data. + +**Endpoint**: `/training-data` +**Method**: GET + +**Query Parameters**: +- `content_type` (optional): Filter by content type +- `page` (optional): Page number for pagination +- `limit` (optional): Number of items per page + +**Response**: +```json +{ + "items": [ + { + "id": "12345", + "content_type": "email_campaign", + "preview": "Dear valued client, We're thrilled to announce...", + "added_at": "2025-04-10T14:30:00Z" + }, + { + "id": "12346", + "content_type": "social_media", + "preview": "Exciting news! Our revolutionary new product...", + "added_at": "2025-04-11T09:15:00Z" + } + ], + "pagination": { + "total": 45, + "page": 1, + "limit": 10, + "pages": 5 + } +} +``` + +## Error Handling + +All endpoints return standard HTTP status codes: + +- `200 OK`: Request successful +- `400 Bad Request`: Invalid request parameters +- `401 Unauthorized`: Authentication failed +- `404 Not Found`: Resource not found +- `500 Internal Server Error`: Server-side error + +Error response format: +```json +{ + "status": "error", + "message": "Detailed error message", + "error_code": "ERROR_CODE" +} +``` \ No newline at end of file diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..2d8bf8c --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,513 @@ +// DOM Elements +document.addEventListener('DOMContentLoaded', function() { + // Navigation + const menuItems = document.querySelectorAll('.menu li'); + const pages = document.querySelectorAll('.page'); + + // Generate Content Page + const generateBtn = document.getElementById('generate-btn'); + const promptInput = document.getElementById('prompt'); + const contentTypeSelect = document.getElementById('content-type'); + const toneSelect = document.getElementById('tone'); + const lengthSelect = document.getElementById('length'); + const includeCTACheckbox = document.getElementById('include-cta'); + const referenceSimilarCheckbox = document.getElementById('reference-similar'); + const resultContainer = document.getElementById('result-container'); + const resultContent = document.getElementById('result-content'); + const loadingIndicator = document.getElementById('loading-indicator'); + const alignmentScore = document.getElementById('alignment-score'); + const suggestionsList = document.getElementById('suggestions-list'); + const copyBtn = document.getElementById('copy-btn'); + const improveBtn = document.getElementById('improve-btn'); + const saveBtn = document.getElementById('save-btn'); + const improvementPanel = document.getElementById('improvement-panel'); + const improvementFeedback = document.getElementById('improvement-feedback'); + const submitImprovement = document.getElementById('submit-improvement'); + + // Brand Style Page + const toneSelector = document.getElementById('tone-selector'); + const voiceSelector = document.getElementById('voice-selector'); + const tabooWords = document.getElementById('taboo-words'); + const tabooInput = document.getElementById('taboo-input'); + const addTabooBtn = document.getElementById('add-taboo-btn'); + const avoidTerm = document.getElementById('avoid-term'); + const useTerm = document.getElementById('use-term'); + const addTermBtn = document.getElementById('add-term-btn'); + const saveBrandStyleBtn = document.getElementById('save-brand-style'); + const resetBrandStyleBtn = document.getElementById('reset-brand-style'); + + // Training Page + const trainingTabs = document.querySelectorAll('.training-tabs .tab'); + const tabContents = document.querySelectorAll('.tab-content'); + const addTrainingBtn = document.getElementById('add-training-btn'); + const trainingContentType = document.getElementById('training-content-type'); + const campaignName = document.getElementById('campaign-name'); + const trainingContent = document.getElementById('training-content'); + const openRate = document.getElementById('open-rate'); + const clickRate = document.getElementById('click-rate'); + const conversionRate = document.getElementById('conversion-rate'); + + // API Base URL + const API_URL = 'http://localhost:8000'; + + // Menu Navigation + menuItems.forEach(item => { + item.addEventListener('click', function() { + const pageName = this.getAttribute('data-page'); + + // Update active menu item + menuItems.forEach(menuItem => menuItem.classList.remove('active')); + this.classList.add('active'); + + // Show selected page + pages.forEach(page => { + if (page.id === `${pageName}-page`) { + page.classList.add('active'); + } else { + page.classList.remove('active'); + } + }); + }); + }); + + // Generate Content + if (generateBtn) { + generateBtn.addEventListener('click', function() { + if (!promptInput.value.trim()) { + alert('Please enter a prompt for content generation.'); + return; + } + + // Show loading indicator + loadingIndicator.classList.remove('hidden'); + resultContainer.classList.add('hidden'); + + // Prepare request data + const requestData = { + prompt: promptInput.value, + content_type: contentTypeSelect.value || null, + tone: toneSelect.value || null, + length: lengthSelect.value || null, + include_cta: includeCTACheckbox.checked, + reference_similar_content: referenceSimilarCheckbox.checked + }; + + // Call the API + fetch(`${API_URL}/generate-copy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + // Hide loading indicator + loadingIndicator.classList.add('hidden'); + + // Display result + resultContent.textContent = data.content; + + // Set alignment score + const score = data.metadata.alignment_score || 0; + alignmentScore.style.width = `${score}%`; + alignmentScore.textContent = `${Math.round(score)}%`; + + if (score < 60) { + alignmentScore.style.backgroundColor = 'var(--danger-color)'; + } else if (score < 80) { + alignmentScore.style.backgroundColor = 'var(--warning-color)'; + } else { + alignmentScore.style.backgroundColor = 'var(--success-color)'; + } + + // Display suggestions + if (data.suggestions && data.suggestions.length > 0) { + suggestionsList.innerHTML = ''; + data.suggestions.forEach(suggestion => { + const li = document.createElement('li'); + li.textContent = suggestion; + li.addEventListener('click', function() { + promptInput.value = suggestion; + }); + suggestionsList.appendChild(li); + }); + } + + // Show result container + resultContainer.classList.remove('hidden'); + }) + .catch(error => { + console.error('Error:', error); + loadingIndicator.classList.add('hidden'); + alert('An error occurred while generating content. Please try again.'); + }); + }); + } + + // Copy to Clipboard + if (copyBtn) { + copyBtn.addEventListener('click', function() { + navigator.clipboard.writeText(resultContent.textContent) + .then(() => { + const originalTitle = copyBtn.getAttribute('title'); + copyBtn.setAttribute('title', 'Copied!'); + setTimeout(() => { + copyBtn.setAttribute('title', originalTitle); + }, 2000); + }) + .catch(err => { + console.error('Could not copy text: ', err); + }); + }); + } + + // Toggle Improvement Panel + if (improveBtn) { + improveBtn.addEventListener('click', function() { + improvementPanel.classList.toggle('hidden'); + }); + } + + // Submit Improvement Feedback + if (submitImprovement) { + submitImprovement.addEventListener('click', function() { + if (!improvementFeedback.value.trim()) { + alert('Please enter feedback for improvement.'); + return; + } + + // Show loading indicator + loadingIndicator.classList.remove('hidden'); + + // Prepare request data + const requestData = { + content: resultContent.textContent, + feedback: improvementFeedback.value + }; + + // Call the API + fetch(`${API_URL}/improve-content`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + // Hide loading indicator + loadingIndicator.classList.add('hidden'); + + // Update result content + resultContent.textContent = data.improved_content; + + // Hide improvement panel + improvementPanel.classList.add('hidden'); + improvementFeedback.value = ''; + }) + .catch(error => { + console.error('Error:', error); + loadingIndicator.classList.add('hidden'); + alert('An error occurred while improving content. Please try again.'); + }); + }); + } + + // Save Content to History + if (saveBtn) { + saveBtn.addEventListener('click', function() { + alert('Content saved to history!'); + // In a real implementation, you would save this to local storage + // or call an API to save it to the backend + }); + } + + // Brand Style Tag Selection + if (toneSelector) { + const tagElements = toneSelector.querySelectorAll('.tag'); + tagElements.forEach(tag => { + tag.addEventListener('click', function() { + this.classList.toggle('selected'); + }); + }); + } + + if (voiceSelector) { + const tagElements = voiceSelector.querySelectorAll('.tag'); + tagElements.forEach(tag => { + tag.addEventListener('click', function() { + this.classList.toggle('selected'); + }); + }); + } + + // Add Taboo Word + if (addTabooBtn && tabooInput && tabooWords) { + addTabooBtn.addEventListener('click', function() { + const word = tabooInput.value.trim(); + if (word) { + const tagElement = document.createElement('span'); + tagElement.classList.add('tag', 'removable'); + tagElement.innerHTML = `${word}`; + + // Add click event to remove the tag + tagElement.querySelector('i').addEventListener('click', function() { + tagElement.remove(); + }); + + tabooWords.appendChild(tagElement); + tabooInput.value = ''; + } + }); + + // Allow pressing Enter to add a word + tabooInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + addTabooBtn.click(); + } + }); + } + + // Add Terminology Term + if (addTermBtn && avoidTerm && useTerm) { + addTermBtn.addEventListener('click', function() { + const avoid = avoidTerm.value.trim(); + const use = useTerm.value.trim(); + + if (avoid && use) { + const tableRow = document.createElement('div'); + tableRow.classList.add('terminology-row'); + tableRow.innerHTML = ` +
${avoid}
+
${use}
+
+ +
+ `; + + // Add click event to remove the row + tableRow.querySelector('.btn-icon').addEventListener('click', function() { + tableRow.remove(); + }); + + // Insert before the add row + const addRow = document.querySelector('.terminology-row.add-row'); + addRow.parentNode.insertBefore(tableRow, addRow); + + avoidTerm.value = ''; + useTerm.value = ''; + } + }); + + // Allow pressing Enter to add a term + useTerm.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + addTermBtn.click(); + } + }); + } + + // Save Brand Style + if (saveBrandStyleBtn) { + saveBrandStyleBtn.addEventListener('click', function() { + // Collect tone tags + const selectedTones = []; + toneSelector.querySelectorAll('.tag.selected').forEach(tag => { + selectedTones.push(tag.textContent); + }); + + // Collect voice characteristics + const selectedVoice = []; + voiceSelector.querySelectorAll('.tag.selected').forEach(tag => { + selectedVoice.push(tag.textContent); + }); + + // Collect taboo words + const tabooWordsList = []; + tabooWords.querySelectorAll('.tag').forEach(tag => { + // Extract just the text without the 'x' icon + const text = tag.textContent.replace('×', '').trim(); + tabooWordsList.push(text); + }); + + // Collect preferred terms + const preferredTerms = {}; + document.querySelectorAll('.terminology-row:not(.add-row):not(.terminology-header)').forEach(row => { + const avoid = row.querySelector('.term-avoid').textContent.trim(); + const use = row.querySelector('.term-use').textContent.trim(); + if (avoid && use) { + preferredTerms[avoid] = use; + } + }); + + // Prepare request data + const requestData = { + tone: selectedTones, + voice_characteristics: selectedVoice, + taboo_words: tabooWordsList, + preferred_terms: preferredTerms + }; + + // Call the API + fetch(`${API_URL}/brand-style`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + alert('Brand style updated successfully!'); + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while updating brand style. Please try again.'); + }); + }); + } + + // Reset Brand Style + if (resetBrandStyleBtn) { + resetBrandStyleBtn.addEventListener('click', function() { + if (confirm('Are you sure you want to reset brand style to defaults?')) { + // In a real implementation, you would call an API to reset + // For now, just reload the page + window.location.reload(); + } + }); + } + + // Training Tabs + if (trainingTabs.length > 0) { + trainingTabs.forEach(tab => { + tab.addEventListener('click', function() { + const tabName = this.getAttribute('data-tab'); + + // Update active tab + trainingTabs.forEach(t => t.classList.remove('active')); + this.classList.add('active'); + + // Show selected tab content + tabContents.forEach(content => { + if (content.id === `${tabName}-tab`) { + content.classList.add('active'); + } else { + content.classList.remove('active'); + } + }); + }); + }); + } + + // Add Training Data + if (addTrainingBtn) { + addTrainingBtn.addEventListener('click', function() { + if (!trainingContentType.value) { + alert('Please select a content type.'); + return; + } + + if (!trainingContent.value.trim()) { + alert('Please enter content for training.'); + return; + } + + // Prepare request data + const requestData = { + content_type: trainingContentType.value, + content: trainingContent.value, + metadata: { + campaign_name: campaignName.value, + performance_metrics: {} + } + }; + + // Add performance metrics if provided + if (openRate.value) { + requestData.metadata.performance_metrics.open_rate = parseFloat(openRate.value) / 100; + } + + if (clickRate.value) { + requestData.metadata.performance_metrics.click_rate = parseFloat(clickRate.value) / 100; + } + + if (conversionRate.value) { + requestData.metadata.performance_metrics.conversion_rate = parseFloat(conversionRate.value) / 100; + } + + // Call the API + fetch(`${API_URL}/training-data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + alert('Training data added successfully!'); + + // Clear form + trainingContentType.value = ''; + campaignName.value = ''; + trainingContent.value = ''; + openRate.value = ''; + clickRate.value = ''; + conversionRate.value = ''; + + // Switch to view tab + document.querySelector('.tab[data-tab="view-training"]').click(); + + // In a real implementation, you would also refresh the training data list + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while adding training data. Please try again.'); + }); + }); + } + + // Load Brand Style on Page Load + fetch(`${API_URL}/brand-style`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + console.log('Loaded brand style:', data); + // In a real implementation, you would update the UI based on the loaded data + }) + .catch(error => { + console.error('Error loading brand style:', error); + }); + + // For demonstration purposes, let's create a mocked pre-filled content + // In a real implementation, this would be loaded from the backend + document.getElementById('prompt').value = 'Write a social media post about our new coaching program'; +}); \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2d8977b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,498 @@ + + + + + + Adriana James - Marketing Assistant AI + + + + + +
+ + +
+
+

Marketing Assistant AI

+
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+
+ + + + +
+ + +
+ + +
+
+
+

Email Welcome Sequence

+

A 5-part email sequence for new subscribers.

+ +
+
+
+

Product Launch Posts

+

Social media templates for product launches.

+ +
+
+
+

Transformation Story

+

Client success story blog post template.

+ +
+
+
+

Workshop Promotion

+

Ad copy template for promoting workshops.

+ +
+
+
+

Create New Template

+

Build a custom template from scratch.

+ +
+
+
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+

Transformation Masterclass Invitation

+

Subject: Transform Your Potential with Adriana James' Exclusive Workshop...

+
+
Apr 17, 2025
+
+ + + +
+
+
+ +
+

3-Step Framework Post

+

BREAKTHROUGH MOMENT ✨ Ever feel stuck in patterns that hold you back...

+
+
Apr 16, 2025
+
+ + + +
+
+
+
Blog
+
+

5 Ways to Overcome Limiting Beliefs

+

Are limiting beliefs holding you back from achieving your full potential?...

+
+
Apr 14, 2025
+
+ + + +
+
+
+
+ + +
+ + +
+
+

Brand Tone

+

Select the tone options that best represent the brand.

+
+ professional + friendly + inspirational + empowering + excited + authoritative + casual + humorous +
+
+ +
+

Voice Characteristics

+

Define the key characteristics of the brand voice.

+
+ clear + direct + empowering + confident + authentic + innovative + visionary + approachable +
+
+ +
+

Taboo Words

+

Words to avoid in all marketing content.

+
+
+ cheap + discount + bargain + failure + impossible + difficult +
+
+ + +
+
+
+ +
+

Preferred Terminology

+

Preferred terms to use instead of common alternatives.

+
+
+
Avoid
+
Use Instead
+
+
+
+
customers
+
clients
+
+ +
+
+
+
products
+
solutions
+
+ +
+
+
+
problems
+
challenges
+
+ +
+
+
+
services
+
experiences
+
+ +
+
+
+
training
+
transformation
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ + +
+
+
+ + +
+ + +
+
Add Training Data
+
View Training Data
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+

Performance Metrics

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+

Transformation Masterclass Promotion

+

Added on: Apr 15, 2025

+
+ Open Rate: 42% + Click Rate: 18% + Conversion: 8% +
+
+
+ + +
+
+
+ +
+

Breakthrough Framework

+

Added on: Apr 10, 2025

+
+ Engagement: 6.4% + Saves: 178 + Shares: 92 +
+
+
+ + +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..4661797 --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,964 @@ +/* Global Variables */ +:root { + --primary-color: #6236FF; + --primary-light: #8E6FFF; + --primary-dark: #4B2AD8; + --secondary-color: #FFB800; + --success-color: #1AC888; + --danger-color: #FF4757; + --warning-color: #FFBA00; + --dark-color: #161925; + --grey-100: #F5F7FA; + --grey-200: #E4E8F0; + --grey-300: #CBD2E0; + --grey-400: #9AA5B9; + --grey-500: #6B7A99; + --grey-600: #4A5568; + --grey-700: #2D3748; + --grey-800: #1A202C; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.15); + --font-family: 'Poppins', sans-serif; +} + +/* Reset & Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-family); + font-size: 15px; + line-height: 1.5; + color: var(--grey-700); + background-color: var(--grey-100); +} + +h1, h2, h3, h4, h5, h6 { + margin-bottom: 0.5rem; + font-weight: 600; + line-height: 1.2; + color: var(--grey-800); +} + +a { + color: var(--primary-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Layout */ +.app-container { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 240px; + background-color: var(--grey-800); + display: flex; + flex-direction: column; + color: white; + padding: 20px 0; + position: fixed; + height: 100vh; + box-shadow: var(--shadow-md); +} + +.content { + flex: 1; + margin-left: 240px; + padding: 20px; + max-width: calc(100vw - 240px); +} + +/* Logo */ +.logo { + padding: 10px 20px 30px; + text-align: center; +} + +.logo h2 { + color: white; + background-color: var(--primary-color); + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + font-size: 26px; + font-weight: 700; +} + +/* Menu */ +.menu { + list-style: none; + margin-bottom: auto; +} + +.menu li { + padding: 12px 20px; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; + color: var(--grey-300); + margin-bottom: 4px; + border-left: 3px solid transparent; +} + +.menu li i { + margin-right: 12px; + font-size: 16px; + width: 20px; + text-align: center; +} + +.menu li:hover { + background-color: rgba(255, 255, 255, 0.05); + color: white; +} + +.menu li.active { + background-color: rgba(255, 255, 255, 0.1); + color: white; + border-left: 3px solid var(--primary-color); +} + +/* User Info */ +.user-info { + display: flex; + align-items: center; + padding: 16px 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin-top: auto; +} + +.user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + overflow: hidden; + margin-right: 12px; +} + +.user-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.user-name { + font-weight: 500; + font-size: 14px; +} + +/* Header */ +header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid var(--grey-200); +} + +.header-actions { + display: flex; + gap: 10px; +} + +/* Page */ +.page { + display: none; +} + +.page.active { + display: block; +} + +.page-header { + margin-bottom: 30px; +} + +.page-header h2 { + margin-bottom: 8px; + font-size: 24px; +} + +.page-header p { + color: var(--grey-500); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border-radius: var(--radius-md); + border: none; + font-weight: 500; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.btn i { + margin-right: 8px; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-dark); +} + +.btn-secondary { + background-color: var(--grey-200); + color: var(--grey-700); +} + +.btn-secondary:hover { + background-color: var(--grey-300); +} + +.btn-outline { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); +} + +.btn-outline:hover { + background-color: var(--primary-color); + color: white; +} + +.btn-lg { + padding: 12px 20px; + font-size: 16px; +} + +.btn-icon { + padding: 8px; + border-radius: 50%; + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + color: var(--grey-600); + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.btn-icon:hover { + background-color: var(--grey-200); + color: var(--grey-800); +} + +.btn-icon i { + margin-right: 0; +} + +/* Forms */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--grey-700); +} + +.form-group input[type="text"], +.form-group input[type="number"], +.form-group input[type="email"], +.form-group input[type="password"], +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--grey-300); + font-family: var(--font-family); + font-size: 15px; + transition: border-color 0.2s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(98, 54, 255, 0.1); +} + +.form-row { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.form-row .form-group { + flex: 1; + margin-bottom: 0; +} + +.form-actions { + margin-top: 30px; + display: flex; + gap: 15px; +} + +.form-section { + margin-bottom: 30px; +} + +.form-section h3 { + font-size: 18px; + margin-bottom: 12px; +} + +.form-section p { + margin-bottom: 15px; + color: var(--grey-500); +} + +/* Checkbox */ +.checkbox-group { + display: flex; + gap: 20px; +} + +.checkbox { + display: flex; + align-items: center; + position: relative; + padding-left: 30px; + cursor: pointer; + font-weight: normal; + user-select: none; +} + +.checkbox input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 20px; + width: 20px; + background-color: white; + border: 1px solid var(--grey-300); + border-radius: var(--radius-sm); +} + +.checkbox:hover input ~ .checkmark { + border-color: var(--grey-400); +} + +.checkbox input:checked ~ .checkmark { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +.checkbox input:checked ~ .checkmark:after { + display: block; +} + +.checkbox .checkmark:after { + left: 7px; + top: 3px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* Generation Form */ +.generation-form { + background-color: white; + border-radius: var(--radius-lg); + padding: 25px; + box-shadow: var(--shadow-sm); + margin-bottom: 30px; +} + +/* Result Container */ +.result-container { + background-color: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + margin-top: 30px; +} + +.result-container.hidden { + display: none; +} + +.result-header { + padding: 20px 25px; + border-bottom: 1px solid var(--grey-200); + display: flex; + justify-content: space-between; + align-items: center; +} + +.result-header h3 { + margin-bottom: 0; +} + +.result-actions { + display: flex; + gap: 10px; +} + +.result-content { + padding: 25px; + white-space: pre-wrap; + line-height: 1.6; +} + +.metadata-panel { + background-color: var(--grey-100); + padding: 20px 25px; + border-top: 1px solid var(--grey-200); +} + +.metadata-item { + margin-bottom: 15px; +} + +.metadata-label { + display: block; + font-weight: 500; + margin-bottom: 5px; + color: var(--grey-600); +} + +.score-bar { + height: 8px; + background-color: var(--grey-200); + border-radius: 4px; + overflow: hidden; +} + +.score-fill { + height: 100%; + background-color: var(--success-color); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 10px; + font-weight: 700; + transition: width 0.5s ease; +} + +.suggestions-container { + margin-top: 20px; +} + +.suggestions-container h4 { + margin-bottom: 10px; + color: var(--grey-600); + font-size: 15px; +} + +.suggestions-list { + list-style: none; +} + +.suggestions-list li { + margin-bottom: 8px; + padding: 8px 12px; + background-color: white; + border-radius: var(--radius-md); + border: 1px solid var(--grey-200); + cursor: pointer; + transition: all 0.2s; +} + +.suggestions-list li:hover { + border-color: var(--primary-color); + background-color: rgba(98, 54, 255, 0.05); +} + +.improvement-panel { + padding: 20px 25px; + border-top: 1px solid var(--grey-200); +} + +.improvement-panel.hidden { + display: none; +} + +.improvement-panel h4 { + margin-bottom: 10px; +} + +/* Loading Indicator */ +.loading-indicator { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 40px 0; +} + +.loading-indicator.hidden { + display: none; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--grey-200); + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 15px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Templates Grid */ +.templates-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.template-card { + background-color: white; + border-radius: var(--radius-lg); + padding: 25px; + box-shadow: var(--shadow-sm); + transition: all 0.2s; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.template-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-md); +} + +.template-icon { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: var(--primary-light); + color: white; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 15px; + font-size: 24px; +} + +.template-card h3 { + margin-bottom: 8px; +} + +.template-card p { + color: var(--grey-500); + margin-bottom: 20px; +} + +.template-card button { + margin-top: auto; +} + +.template-card.template-add { + border: 2px dashed var(--grey-300); + background-color: transparent; +} + +.template-card.template-add .template-icon { + background-color: var(--grey-300); +} + +.template-card.template-add:hover { + border-color: var(--primary-color); +} + +/* History and Training List */ +.history-filters, +.training-filters { + display: flex; + gap: 15px; + margin-bottom: 20px; +} + +.history-filters .form-group, +.training-filters .form-group { + margin-bottom: 0; + flex: 1; +} + +.history-list, +.training-list { + background-color: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.history-item, +.training-item { + display: flex; + padding: 15px 20px; + border-bottom: 1px solid var(--grey-200); + align-items: center; +} + +.history-item:last-child, +.training-item:last-child { + border-bottom: none; +} + +.history-item-type, +.training-item-type { + width: 80px; + text-align: center; + padding: 4px 8px; + border-radius: var(--radius-md); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.history-item-type.email_campaign, +.training-item-type.email_campaign { + background-color: var(--primary-light); + color: white; +} + +.history-item-type.social_media, +.training-item-type.social_media { + background-color: var(--secondary-color); + color: white; +} + +.history-item-type.blog_post, +.training-item-type.blog_post { + background-color: var(--success-color); + color: white; +} + +.history-item-type.website_copy, +.training-item-type.website_copy { + background-color: var(--warning-color); + color: white; +} + +.history-item-content, +.training-item-content { + flex: 1; + padding: 0 20px; +} + +.history-item-content h4, +.training-item-content h4 { + margin-bottom: 5px; +} + +.history-item-content p, +.training-item-content p { + color: var(--grey-500); + font-size: 14px; +} + +.history-item-date { + color: var(--grey-500); + font-size: 14px; + min-width: 100px; + text-align: right; + margin-right: 15px; +} + +.history-item-actions, +.training-item-actions { + display: flex; + gap: 5px; +} + +.metrics { + display: flex; + gap: 10px; + margin-top: 5px; +} + +.metric { + font-size: 13px; + color: var(--grey-600); + background-color: var(--grey-100); + padding: 2px 8px; + border-radius: var(--radius-sm); +} + +/* Brand Style */ +.tag-selector { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +.tag { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background-color: var(--grey-200); + color: var(--grey-700); + border-radius: 20px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.tag.selected { + background-color: var(--primary-color); + color: white; +} + +.tag.removable { + padding-right: 8px; +} + +.tag.removable i { + margin-left: 8px; + font-size: 12px; +} + +.tag-editor { + margin-top: 10px; +} + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; +} + +.tag-input-container { + display: flex; + gap: 10px; +} + +.tag-input-container input { + flex: 1; +} + +.terminology-table { + margin-top: 15px; + border: 1px solid var(--grey-200); + border-radius: var(--radius-md); + overflow: hidden; +} + +.terminology-header { + display: flex; + background-color: var(--grey-100); + padding: 12px 15px; + font-weight: 600; + border-bottom: 1px solid var(--grey-200); +} + +.terminology-row { + display: flex; + padding: 12px 15px; + border-bottom: 1px solid var(--grey-200); + align-items: center; +} + +.terminology-row:last-child { + border-bottom: none; +} + +.term-avoid { + flex: 1; +} + +.term-use { + flex: 1; +} + +.term-actions { + width: 50px; + text-align: right; +} + +.terminology-row.add-row input { + width: 100%; + border: none; + padding: 5px 0; + background: transparent; +} + +.terminology-row.add-row input:focus { + outline: none; + box-shadow: none; +} + +/* Training Tabs */ +.training-tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 1px solid var(--grey-200); +} + +.tab { + padding: 12px 20px; + cursor: pointer; + position: relative; + color: var(--grey-500); +} + +.tab.active { + color: var(--primary-color); + font-weight: 500; +} + +.tab.active:after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background-color: var(--primary-color); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Responsive */ +@media (max-width: 992px) { + .sidebar { + width: 80px; + padding: 15px 0; + } + + .logo h2 { + width: 50px; + height: 50px; + font-size: 20px; + } + + .menu li { + justify-content: center; + padding: 12px; + } + + .menu li i { + margin-right: 0; + font-size: 20px; + } + + .menu li span { + display: none; + } + + .user-info { + justify-content: center; + padding: 10px; + } + + .user-avatar { + margin-right: 0; + } + + .user-name { + display: none; + } + + .content { + margin-left: 80px; + max-width: calc(100vw - 80px); + } +} + +@media (max-width: 768px) { + .form-row { + flex-direction: column; + gap: 10px; + } + + .templates-grid { + grid-template-columns: 1fr; + } + + .history-item, + .training-item { + flex-direction: column; + align-items: flex-start; + } + + .history-item-type, + .training-item-type { + margin-bottom: 10px; + } + + .history-item-content, + .training-item-content { + padding: 0; + margin-bottom: 10px; + } + + .history-item-date { + text-align: left; + margin-bottom: 10px; + } + + .checkbox-group { + flex-direction: column; + gap: 10px; + } +} \ No newline at end of file diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 0000000..d055e92 --- /dev/null +++ b/logs/app.log @@ -0,0 +1,460 @@ +2025-04-17 07:08:51.543 | WARNING | vector_store:search:159 - Empty vector store, no results to return +2025-04-17 07:08:51.543 | WARNING | vector_store:search:159 - Empty vector store, no results to return +2025-04-17 07:08:51.551 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters +2025-04-17 07:08:51.551 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters +2025-04-17 07:08:52.803 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:08:52.803 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:12:18.934 | INFO | vector_store:search:212 - Found 1 matching documents for query +2025-04-17 07:12:18.934 | INFO | vector_store:search:212 - Found 1 matching documents for query +2025-04-17 07:12:18.936 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters +2025-04-17 07:12:18.936 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters +2025-04-17 07:12:19.677 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:12:19.677 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:15:03.309 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:03.309 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:05.643 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:05.643 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:05.644 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:05.644 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:10.452 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:10.452 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:10.455 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:10.455 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:15.166 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:15.166 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:15.168 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:15.168 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:15.170 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:15.170 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:20.280 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:20.280 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:21.317 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:21.317 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:21.369 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:21.369 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:26.051 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:26.051 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:26.052 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:26.052 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:30.842 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:30.842 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:30.847 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:30.847 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:30.859 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:30.859 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:36.115 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:36.115 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:15:36.882 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:36.882 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:36.885 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:36.885 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:41.549 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:41.549 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:41.551 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:41.551 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:46.258 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:46.258 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, { + "error": { + "message": "The model `gpt-4` does not exist or you do not have access to it.", + "type": "invalid_request_error", + "param": null, + "code": "model_not_found" + } +} + +2025-04-17 07:15:46.266 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:46.266 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404 +2025-04-17 07:15:46.269 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:46.269 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:15:46.274 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[] +2025-04-17 07:15:46.274 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[] +2025-04-17 07:18:54.993 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:18:54.993 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:18:57.991 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:18:57.991 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:18:57.993 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:18:57.993 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:02.717 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:02.717 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:02.719 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:02.719 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:07.525 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:07.525 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:07.526 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:07.526 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:07.527 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:07.527 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:12.302 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:19:12.302 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:19:13.063 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:13.063 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:13.064 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:13.064 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:21.192 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:21.192 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:21.199 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:21.199 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:26.353 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:26.353 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:26.360 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:26.360 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:26.364 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:26.364 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:31.480 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:19:31.480 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:19:32.593 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:32.593 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:32.597 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:32.597 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:37.418 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:37.418 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:37.425 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:37.425 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:42.179 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:42.179 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, { + "error": { + "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", + "type": "insufficient_quota", + "param": null, + "code": "insufficient_quota" + } +} + +2025-04-17 07:19:42.180 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:42.180 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429 +2025-04-17 07:19:42.181 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:42.181 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[] +2025-04-17 07:19:42.182 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[] +2025-04-17 07:19:42.182 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[] +2025-04-17 07:23:26.426 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:23:26.426 | INFO | vector_store:search:212 - Found 2 matching documents for query +2025-04-17 07:23:41.296 | INFO | copywriter:generate_copy:118 - Generated content with 1092 characters +2025-04-17 07:23:41.296 | INFO | copywriter:generate_copy:118 - Generated content with 1092 characters +2025-04-17 07:23:41.800 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:23:41.800 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:24:54.053 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:24:54.053 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:25:04.622 | INFO | copywriter:generate_copy:118 - Generated content with 1528 characters +2025-04-17 07:25:04.622 | INFO | copywriter:generate_copy:118 - Generated content with 1528 characters +2025-04-17 07:25:05.154 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:25:05.154 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:36:21.399 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:36:21.399 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:36:38.021 | INFO | copywriter:generate_copy:118 - Generated content with 1506 characters +2025-04-17 07:36:38.021 | INFO | copywriter:generate_copy:118 - Generated content with 1506 characters +2025-04-17 07:36:38.691 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:36:38.691 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:52:38.745 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:52:38.745 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:52:43.989 | INFO | copywriter:generate_copy:118 - Generated content with 735 characters +2025-04-17 07:52:43.989 | INFO | copywriter:generate_copy:118 - Generated content with 735 characters +2025-04-17 07:52:44.389 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:52:44.389 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:53:48.816 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 07:53:48.816 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 07:53:53.715 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 07:53:53.715 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 07:57:41.845 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:57:41.845 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 07:57:49.623 | INFO | copywriter:generate_copy:118 - Generated content with 1037 characters +2025-04-17 07:57:49.623 | INFO | copywriter:generate_copy:118 - Generated content with 1037 characters +2025-04-17 07:57:49.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:57:49.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:58:37.795 | INFO | copywriter:generate_copy:118 - Generated content with 1229 characters +2025-04-17 07:58:37.795 | INFO | copywriter:generate_copy:118 - Generated content with 1229 characters +2025-04-17 07:58:38.334 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 07:58:38.334 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:00:19.501 | INFO | copywriter:improve_copy:221 - Improved content based on feedback +2025-04-17 08:00:19.501 | INFO | copywriter:improve_copy:221 - Improved content based on feedback +2025-04-17 08:02:10.367 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 08:02:10.367 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 08:03:00.533 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 08:03:00.533 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 08:03:15.382 | INFO | copywriter:generate_copy:118 - Generated content with 2057 characters +2025-04-17 08:03:15.382 | INFO | copywriter:generate_copy:118 - Generated content with 2057 characters +2025-04-17 08:03:15.964 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:03:15.964 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:04:49.387 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 08:04:49.387 | INFO | vector_store:search:212 - Found 3 matching documents for query +2025-04-17 08:05:19.792 | ERROR | copywriter:_call_llm_api:167 - Error calling Cohere API: +2025-04-17 08:05:19.792 | ERROR | copywriter:_call_llm_api:167 - Error calling Cohere API: +2025-04-17 08:05:33.019 | INFO | copywriter:generate_copy:118 - Generated content with 938 characters +2025-04-17 08:05:33.019 | INFO | copywriter:generate_copy:118 - Generated content with 938 characters +2025-04-17 08:05:33.540 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:05:33.540 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:08:22.724 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 08:08:22.724 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines +2025-04-17 08:10:36.577 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-17 08:10:36.577 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store