ALwrity + Wordpress + Wix + GSC integration
This commit is contained in:
167
.github/README.md
vendored
167
.github/README.md
vendored
@@ -29,10 +29,59 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
- **🌍 Multi-Modal Content Creation**: Text, images, audio, and video content generation
|
- **🌍 Multi-Modal Content Creation**: Text, images, audio, and video content generation
|
||||||
- **📊 Data-Driven Insights**: Web research, competitor analysis, and predictive analytics
|
- **📊 Data-Driven Insights**: Web research, competitor analysis, and predictive analytics
|
||||||
- **🤖 AI Agent Teams**: Specialized AI agents for different marketing tasks
|
- **🤖 AI Agent Teams**: Specialized AI agents for different marketing tasks
|
||||||
- **🔗 Platform Integration**: Direct publishing to WordPress, social media, and more
|
- **🔗 Platform Integration**: Direct publishing to WordPress, Wix, Google Search Console, and more
|
||||||
- **📈 Performance Optimization**: Continuous learning and strategy refinement
|
- **📈 Performance Optimization**: Continuous learning and strategy refinement
|
||||||
- **🎯 Solopreneur-Focused**: Designed specifically for independent entrepreneurs
|
- **🎯 Solopreneur-Focused**: Designed specifically for independent entrepreneurs
|
||||||
- **🛡️ Enterprise Security**: JWT authentication, rate limiting, and comprehensive monitoring
|
- **🛡️ Enterprise Security**: JWT authentication, rate limiting, and comprehensive monitoring
|
||||||
|
- **✨ Intelligent Onboarding**: AI-powered setup process that analyzes your business and generates personalized strategies
|
||||||
|
|
||||||
|
### 🚀 **NEW: Complete Onboarding & Integration System**
|
||||||
|
|
||||||
|
**Transform Your Digital Presence in 5 Simple Steps:**
|
||||||
|
|
||||||
|
1. **📧 Email Setup & Business Analysis** - AI analyzes your business domain and industry
|
||||||
|
2. **🎭 AI Persona Generation** - Creates detailed buyer personas and audience insights
|
||||||
|
3. **🏢 Business Information Collection** - Gathers comprehensive business data for personalized strategies
|
||||||
|
4. **🔍 Competitor Analysis** - Real-time competitor research and market positioning
|
||||||
|
5. **🔗 Platform Integrations** - Connect WordPress, Wix, Google Search Console with OAuth security
|
||||||
|
|
||||||
|
**🎯 User Impact**: Go from zero to fully optimized digital presence in under 15 minutes!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started (Live Now!)
|
||||||
|
|
||||||
|
### **⚡ Quick Start - 3 Steps to Success**
|
||||||
|
|
||||||
|
**1. Clone & Setup (2 minutes)**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AJaySi/AI-Writer.git
|
||||||
|
cd AI-Writer/backend && pip install -r requirements.txt
|
||||||
|
cd ../frontend && npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Launch Platform (1 minute)**
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Backend
|
||||||
|
cd backend && python start_alwrity_backend.py
|
||||||
|
|
||||||
|
# Terminal 2: Frontend
|
||||||
|
cd frontend && npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Access & Create (Instant)**
|
||||||
|
- **Frontend**: http://localhost:3000
|
||||||
|
- **API Docs**: http://localhost:8000/api/docs
|
||||||
|
- **Complete onboarding** → **Generate content** → **Publish everywhere**
|
||||||
|
|
||||||
|
### **🎯 What You'll Get Immediately:**
|
||||||
|
- ✅ **AI-powered business analysis** and strategy generation
|
||||||
|
- ✅ **LinkedIn content creation** with fact-checking and Google grounding
|
||||||
|
- ✅ **Blog writing** with research, SEO optimization, and metadata
|
||||||
|
- ✅ **Facebook content generation** with platform-specific optimization
|
||||||
|
- ✅ **WordPress & Wix integration** with OAuth security
|
||||||
|
- ✅ **Google Search Console** analytics and insights
|
||||||
|
- ✅ **Competitor analysis** and market intelligence
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,6 +116,9 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
- **Google Search Console Integration**: OAuth2 authentication and real-time analytics
|
- **Google Search Console Integration**: OAuth2 authentication and real-time analytics
|
||||||
- **Hallucination Detection**: AI-powered fact-checking and content verification
|
- **Hallucination Detection**: AI-powered fact-checking and content verification
|
||||||
- **Persona System**: Advanced writing persona generation and management
|
- **Persona System**: Advanced writing persona generation and management
|
||||||
|
- **Google Grounding**: Real-time fact verification using Google Search API
|
||||||
|
- **Exa AI Integration**: Advanced semantic search and content discovery
|
||||||
|
- **Assistive Writing**: Real-time writing suggestions and optimization
|
||||||
|
|
||||||
### **✅ Frontend Development - COMPLETE**
|
### **✅ Frontend Development - COMPLETE**
|
||||||
- **React Application**: Modern TypeScript-based frontend with Material-UI
|
- **React Application**: Modern TypeScript-based frontend with Material-UI
|
||||||
@@ -75,7 +127,9 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
- **Real-time Updates**: Live progress tracking and notifications
|
- **Real-time Updates**: Live progress tracking and notifications
|
||||||
- **Blog Writer Interface**: Complete WYSIWYG editor with research integration
|
- **Blog Writer Interface**: Complete WYSIWYG editor with research integration
|
||||||
- **SEO Dashboard**: Comprehensive SEO analysis and metadata generation tools
|
- **SEO Dashboard**: Comprehensive SEO analysis and metadata generation tools
|
||||||
- **Onboarding System**: Multi-step guided setup with business information collection
|
- **Complete Onboarding System**: 5-step AI-powered setup with business analysis
|
||||||
|
- **Platform Integrations**: WordPress, Wix, Google Search Console with OAuth
|
||||||
|
- **Coming Soon Section**: Interactive preview of upcoming features
|
||||||
|
|
||||||
### **📅 Launch Timeline**
|
### **📅 Launch Timeline**
|
||||||
- **Current**: Full platform operational with all core features
|
- **Current**: Full platform operational with all core features
|
||||||
@@ -100,11 +154,39 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
|
|
||||||
| **Platform** | **Content Types** | **Status** |
|
| **Platform** | **Content Types** | **Status** |
|
||||||
|--------------|------------------|------------|
|
|--------------|------------------|------------|
|
||||||
| **LinkedIn** | Posts, Articles, Carousels, Video Scripts, Comments, Fact-Checking | ✅ Complete |
|
| **LinkedIn** | Posts, Articles, Carousels, Video Scripts, Comments, Fact-Checking, Google Grounding | ✅ Complete |
|
||||||
| **Facebook** | Posts, Stories, Ads, Community Content | ✅ Complete |
|
| **Facebook** | Posts, Stories, Ads, Community Content | ✅ Complete |
|
||||||
| **Blog Writer** | Research, Outline, Content Generation, SEO Analysis, Metadata | ✅ Complete |
|
| **Blog Writer** | Research, Outline, Content Generation, SEO Analysis, Metadata, Exa AI Integration | ✅ Complete |
|
||||||
| **SEO Content** | Blog posts, landing pages, technical content | ✅ Complete |
|
| **SEO Content** | Blog posts, landing pages, technical content | ✅ Complete |
|
||||||
| **General Content** | Long-form articles, social media posts | ✅ Complete |
|
| **General Content** | Long-form articles, social media posts | ✅ Complete |
|
||||||
|
| **Assistive Writing** | Real-time suggestions, grammar checking, tone optimization | ✅ Complete |
|
||||||
|
|
||||||
|
### 🔍 **Advanced Research & Fact-Checking**
|
||||||
|
|
||||||
|
| **Feature** | **AI Capabilities** | **Status** |
|
||||||
|
|-------------|-------------------|------------|
|
||||||
|
| **Google Grounding** | Real-time fact verification using Google Search | ✅ Complete |
|
||||||
|
| **Exa AI Integration** | Semantic search and content discovery | ✅ Complete |
|
||||||
|
| **Fact-Checking Engine** | AI-powered content verification and source validation | ✅ Complete |
|
||||||
|
| **Web Research** | Automated competitor analysis and market intelligence | ✅ Complete |
|
||||||
|
| **Citation Management** | Automatic source tracking and verification | ✅ Complete |
|
||||||
|
|
||||||
|
### 🚀 **Complete Onboarding System**
|
||||||
|
|
||||||
|
| **Step** | **AI Capabilities** | **User Impact** |
|
||||||
|
|----------|-------------------|-----------------|
|
||||||
|
| **📧 Email & Business Analysis** | AI analyzes your domain, industry, and business model | **Instant business insights** and personalized recommendations |
|
||||||
|
| **🎭 AI Persona Generation** | Creates detailed buyer personas with demographic and psychographic data | **Target the right audience** with precision marketing strategies |
|
||||||
|
| **🏢 Business Information** | Collects comprehensive business data for strategy personalization | **Customized content strategies** that align with your business goals |
|
||||||
|
| **🔍 Competitor Analysis** | Real-time competitor research using Exa AI and web scraping | **Stay ahead of competition** with data-driven market positioning |
|
||||||
|
| **🔗 Platform Integrations** | OAuth-secured connections to WordPress, Wix, Google Search Console | **Publish everywhere** with one-click integration and real-time analytics |
|
||||||
|
|
||||||
|
**🎯 User Benefits:**
|
||||||
|
- **15-minute setup** from zero to fully optimized digital presence
|
||||||
|
- **Professional marketing strategy** without hiring agencies
|
||||||
|
- **Automated competitor intelligence** for strategic advantage
|
||||||
|
- **One-click publishing** across all major platforms
|
||||||
|
- **Real-time performance tracking** with actionable insights
|
||||||
|
|
||||||
### 🔍 **Advanced SEO & Technical Optimization**
|
### 🔍 **Advanced SEO & Technical Optimization**
|
||||||
|
|
||||||
@@ -117,6 +199,23 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
| **SEO Metadata** | Automated title, description, Open Graph, Twitter Cards | ✅ Complete |
|
| **SEO Metadata** | Automated title, description, Open Graph, Twitter Cards | ✅ Complete |
|
||||||
| **Google Search Console** | OAuth2 integration, real-time analytics, sitemap analysis | ✅ Complete |
|
| **Google Search Console** | OAuth2 integration, real-time analytics, sitemap analysis | ✅ Complete |
|
||||||
|
|
||||||
|
### 🔗 **Platform Integrations & Publishing**
|
||||||
|
|
||||||
|
| **Integration** | **Features** | **User Impact** |
|
||||||
|
|-----------------|-------------|-----------------|
|
||||||
|
| **WordPress OAuth** | Direct publishing, media management, category/tag sync | **One-click publishing** to WordPress sites with full content optimization |
|
||||||
|
| **Wix Integration** | Blog post creation, media upload, SEO optimization | **Seamless Wix publishing** with automatic SEO metadata generation |
|
||||||
|
| **Google Search Console** | Real-time analytics, search performance, keyword tracking | **Data-driven optimization** with actual search performance insights |
|
||||||
|
| **LinkedIn Publishing** | Direct post creation, article publishing, engagement tracking | **Professional content** published directly to LinkedIn with analytics |
|
||||||
|
| **Facebook Integration** | Post scheduling, media upload, audience targeting | **Social media automation** with platform-specific optimization |
|
||||||
|
|
||||||
|
**🎯 Integration Benefits:**
|
||||||
|
- **OAuth security** - No password sharing, enterprise-grade authentication
|
||||||
|
- **Real-time analytics** - Performance tracking across all platforms
|
||||||
|
- **Automated SEO** - Every published piece optimized for search engines
|
||||||
|
- **Content synchronization** - Consistent branding and messaging across platforms
|
||||||
|
- **Performance insights** - Data-driven content optimization recommendations
|
||||||
|
|
||||||
### 🖼️ **AI Image Generation**
|
### 🖼️ **AI Image Generation**
|
||||||
|
|
||||||
| **Feature** | **Capabilities** | **Status** |
|
| **Feature** | **Capabilities** | **Status** |
|
||||||
@@ -136,6 +235,23 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
| **Automated Optimization** | Continuous strategy refinement | ✅ Complete |
|
| **Automated Optimization** | Continuous strategy refinement | ✅ Complete |
|
||||||
| **Usage Tracking** | Comprehensive API usage and billing analytics | ✅ Complete |
|
| **Usage Tracking** | Comprehensive API usage and billing analytics | ✅ Complete |
|
||||||
|
|
||||||
|
### 🚀 **Coming Soon Features**
|
||||||
|
|
||||||
|
| **Feature** | **Status** | **Expected Impact** |
|
||||||
|
|-------------|------------|-------------------|
|
||||||
|
| **Social Media OAuth** | 🔄 Awaiting Platform Approval | **Automated LinkedIn & Facebook posting** with advanced scheduling |
|
||||||
|
| **Instagram Integration** | 📅 Planned | **Story creation, hashtag optimization, and visual content** |
|
||||||
|
| **Advanced WordPress Features** | 🔄 In Development | **Media library management, advanced SEO tools, auto-publishing** |
|
||||||
|
| **Mobile Application** | 📅 Q2 2025 | **Content creation and management on-the-go** |
|
||||||
|
| **AI Agent Marketplace** | 📅 Q3 2025 | **Specialized AI agents for specific marketing tasks** |
|
||||||
|
| **Enterprise White-Label** | 📅 Q3 2025 | **Customizable platform for agencies and enterprises** |
|
||||||
|
|
||||||
|
**🎯 Future Benefits:**
|
||||||
|
- **Complete social media automation** across all major platforms
|
||||||
|
- **Mobile-first content creation** for busy entrepreneurs
|
||||||
|
- **AI agent ecosystem** for specialized marketing tasks
|
||||||
|
- **Enterprise-grade customization** for agencies and large teams
|
||||||
|
|
||||||
### 🛡️ **Enterprise Features**
|
### 🛡️ **Enterprise Features**
|
||||||
|
|
||||||
| **Feature** | **Capabilities** | **Status** |
|
| **Feature** | **Capabilities** | **Status** |
|
||||||
@@ -195,34 +311,27 @@ ALwrity is a **comprehensive AI-powered digital marketing platform** that revolu
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started (Right Now!)
|
## 🎯 **Start Creating Content Now**
|
||||||
|
|
||||||
### **1. Backend Setup**
|
### **Complete Onboarding Process**
|
||||||
```bash
|
1. **📧 Email Setup** - Enter your business email for AI analysis
|
||||||
cd backend
|
2. **🎭 Persona Generation** - AI creates detailed buyer personas
|
||||||
pip install -r requirements.txt
|
3. **🏢 Business Info** - Provide comprehensive business details
|
||||||
python start_alwrity_backend.py
|
4. **🔍 Competitor Analysis** - AI researches your competition
|
||||||
```
|
5. **🔗 Platform Integration** - Connect WordPress, Wix, GSC
|
||||||
|
|
||||||
### **2. Frontend Setup**
|
### **Immediate Content Creation**
|
||||||
```bash
|
- ✅ **LinkedIn Posts** with fact-checking and Google grounding
|
||||||
cd frontend
|
- ✅ **Blog Writing** with research and SEO optimization
|
||||||
npm install
|
- ✅ **Facebook Content** with platform-specific optimization
|
||||||
npm start
|
- ✅ **SEO Analysis** with metadata generation
|
||||||
```
|
- ✅ **Image Generation** with AI-powered visuals
|
||||||
|
|
||||||
### **3. Access the Platform**
|
### **Publishing & Analytics**
|
||||||
- **Backend API**: http://localhost:8000/api/docs
|
- **One-click publishing** to WordPress and Wix
|
||||||
- **Frontend**: http://localhost:3000
|
- **Real-time analytics** from Google Search Console
|
||||||
- **Health Check**: http://localhost:8000/health
|
- **Performance tracking** across all platforms
|
||||||
|
- **Data-driven optimization** recommendations
|
||||||
### **4. Start Creating Content**
|
|
||||||
- Complete the AI-powered onboarding process
|
|
||||||
- Generate your personalized content strategy
|
|
||||||
- Create content across all platforms
|
|
||||||
- Track performance with real-time analytics
|
|
||||||
- Use the Blog Writer for complete blog creation
|
|
||||||
- Leverage SEO analysis and metadata generation
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
293
DEPLOYMENT_GUIDE.md
Normal file
293
DEPLOYMENT_GUIDE.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# 🚀 ALwrity Deployment Guide
|
||||||
|
|
||||||
|
## Vercel Deployment Strategy
|
||||||
|
|
||||||
|
Since Vercel is optimized for frontend applications, we'll deploy the frontend on Vercel and the backend on a separate platform.
|
||||||
|
|
||||||
|
### **Architecture Overview**
|
||||||
|
```
|
||||||
|
Frontend (Vercel) ←→ Backend (Railway/Render) ←→ Database (SQLite/PostgreSQL)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Step-by-Step Deployment**
|
||||||
|
|
||||||
|
### **Part 1: Deploy Backend (Railway - Recommended)**
|
||||||
|
|
||||||
|
#### **Option A: Railway (Recommended)**
|
||||||
|
|
||||||
|
1. **Install Railway CLI:**
|
||||||
|
```bash
|
||||||
|
npm install -g @railway/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Login to Railway:**
|
||||||
|
```bash
|
||||||
|
railway login
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Initialize Project:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
railway init
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Add Environment Variables:**
|
||||||
|
```bash
|
||||||
|
railway variables set GEMINI_API_KEY=your_gemini_key
|
||||||
|
railway variables set OPENAI_API_KEY=your_openai_key
|
||||||
|
railway variables set ANTHROPIC_API_KEY=your_anthropic_key
|
||||||
|
railway variables set MISTRAL_API_KEY=your_mistral_key
|
||||||
|
railway variables set TAVILY_API_KEY=your_tavily_key
|
||||||
|
railway variables set EXA_API_KEY=your_exa_key
|
||||||
|
railway variables set SERPER_API_KEY=your_serper_key
|
||||||
|
railway variables set CLERK_SECRET_KEY=your_clerk_secret
|
||||||
|
railway variables set GSC_REDIRECT_URI=https://your-frontend.vercel.app/gsc/callback
|
||||||
|
railway variables set WORDPRESS_REDIRECT_URI=https://your-frontend.vercel.app/wp/callback
|
||||||
|
railway variables set WIX_REDIRECT_URI=https://your-frontend.vercel.app/wix/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Deploy:**
|
||||||
|
```bash
|
||||||
|
railway up
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Get Backend URL:**
|
||||||
|
```bash
|
||||||
|
railway domain
|
||||||
|
```
|
||||||
|
Copy the URL (e.g., `https://your-app.railway.app`)
|
||||||
|
|
||||||
|
#### **Option B: Render**
|
||||||
|
|
||||||
|
1. Go to [render.com](https://render.com) and connect your GitHub
|
||||||
|
2. Create a new "Web Service"
|
||||||
|
3. Select your repository
|
||||||
|
4. Configure:
|
||||||
|
- **Build Command:** `pip install -r requirements.txt`
|
||||||
|
- **Start Command:** `python start_alwrity_backend.py`
|
||||||
|
- **Environment:** Python 3
|
||||||
|
5. Add all environment variables in the dashboard
|
||||||
|
6. Deploy and get your backend URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Part 2: Deploy Frontend (Vercel)**
|
||||||
|
|
||||||
|
#### **1. Prepare Frontend for Production**
|
||||||
|
|
||||||
|
Create a `.env.production` file in the `frontend` directory:
|
||||||
|
```env
|
||||||
|
REACT_APP_API_URL=https://your-backend-url.railway.app
|
||||||
|
REACT_APP_ENVIRONMENT=production
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2. Deploy to Vercel**
|
||||||
|
|
||||||
|
1. **Connect to Vercel:**
|
||||||
|
- Go to [vercel.com](https://vercel.com)
|
||||||
|
- Click "New Project"
|
||||||
|
- Import your GitHub repository
|
||||||
|
|
||||||
|
2. **Configure Project:**
|
||||||
|
- **Framework Preset:** Create React App
|
||||||
|
- **Root Directory:** `frontend`
|
||||||
|
- **Build Command:** `npm run build`
|
||||||
|
- **Output Directory:** `build`
|
||||||
|
|
||||||
|
3. **Add Environment Variables:**
|
||||||
|
```
|
||||||
|
REACT_APP_API_URL = https://your-backend-url.railway.app
|
||||||
|
REACT_APP_ENVIRONMENT = production
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy:**
|
||||||
|
- Click "Deploy"
|
||||||
|
- Wait for deployment to complete
|
||||||
|
- Get your frontend URL (e.g., `https://your-app.vercel.app`)
|
||||||
|
|
||||||
|
#### **3. Update Backend URLs**
|
||||||
|
|
||||||
|
After getting your frontend URL, update the backend environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update redirect URIs with your actual Vercel URL
|
||||||
|
railway variables set GSC_REDIRECT_URI=https://your-app.vercel.app/gsc/callback
|
||||||
|
railway variables set WORDPRESS_REDIRECT_URI=https://your-app.vercel.app/wp/callback
|
||||||
|
railway variables set WIX_REDIRECT_URI=https://your-app.vercel.app/wix/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Alternative: Full-Stack on Vercel (Advanced)**
|
||||||
|
|
||||||
|
If you want to deploy everything on Vercel, you can use Vercel's serverless functions:
|
||||||
|
|
||||||
|
### **1. Create API Routes**
|
||||||
|
|
||||||
|
Create `api/` directory in your project root:
|
||||||
|
|
||||||
|
```
|
||||||
|
api/
|
||||||
|
├── auth/
|
||||||
|
│ └── [...auth].ts
|
||||||
|
├── content/
|
||||||
|
│ └── generate.ts
|
||||||
|
└── seo/
|
||||||
|
└── analyze.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Convert FastAPI to Vercel Functions**
|
||||||
|
|
||||||
|
This requires significant refactoring of your backend code to work with Vercel's serverless functions.
|
||||||
|
|
||||||
|
### **3. Deploy as Monorepo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Vercel CLI
|
||||||
|
npm i -g vercel
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
vercel --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ **Environment Variables Checklist**
|
||||||
|
|
||||||
|
### **Backend Environment Variables:**
|
||||||
|
```bash
|
||||||
|
# AI API Keys
|
||||||
|
GEMINI_API_KEY=your_gemini_key
|
||||||
|
OPENAI_API_KEY=your_openai_key
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_key
|
||||||
|
MISTRAL_API_KEY=your_mistral_key
|
||||||
|
|
||||||
|
# Research APIs
|
||||||
|
TAVILY_API_KEY=your_tavily_key
|
||||||
|
EXA_API_KEY=your_exa_key
|
||||||
|
SERPER_API_KEY=your_serper_key
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
CLERK_SECRET_KEY=your_clerk_secret
|
||||||
|
|
||||||
|
# OAuth Redirects (update with your Vercel URL)
|
||||||
|
GSC_REDIRECT_URI=https://your-app.vercel.app/gsc/callback
|
||||||
|
WORDPRESS_REDIRECT_URI=https://your-app.vercel.app/wp/callback
|
||||||
|
WIX_REDIRECT_URI=https://your-app.vercel.app/wix/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Frontend Environment Variables:**
|
||||||
|
```bash
|
||||||
|
REACT_APP_API_URL=https://your-backend-url.railway.app
|
||||||
|
REACT_APP_ENVIRONMENT=production
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 **Post-Deployment Steps**
|
||||||
|
|
||||||
|
### **1. Test Your Deployment**
|
||||||
|
- Visit your Vercel URL
|
||||||
|
- Complete the onboarding process
|
||||||
|
- Test content generation
|
||||||
|
- Verify OAuth integrations
|
||||||
|
|
||||||
|
### **2. Configure Custom Domain (Optional)**
|
||||||
|
- In Vercel dashboard, go to your project
|
||||||
|
- Click "Domains"
|
||||||
|
- Add your custom domain
|
||||||
|
- Update redirect URIs with custom domain
|
||||||
|
|
||||||
|
### **3. Monitor Performance**
|
||||||
|
- Check Vercel analytics
|
||||||
|
- Monitor Railway/Render logs
|
||||||
|
- Set up error tracking (Sentry, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 **Troubleshooting**
|
||||||
|
|
||||||
|
### **Common Issues:**
|
||||||
|
|
||||||
|
1. **CORS Errors:**
|
||||||
|
- Add your Vercel domain to backend CORS settings
|
||||||
|
- Update `allowed_origins` in FastAPI CORS middleware
|
||||||
|
|
||||||
|
2. **Environment Variables:**
|
||||||
|
- Ensure all variables are set in both platforms
|
||||||
|
- Check variable names match exactly
|
||||||
|
|
||||||
|
3. **OAuth Redirects:**
|
||||||
|
- Update all redirect URIs with production URLs
|
||||||
|
- Test OAuth flows end-to-end
|
||||||
|
|
||||||
|
4. **Database Issues:**
|
||||||
|
- Consider upgrading to PostgreSQL for production
|
||||||
|
- Set up database backups
|
||||||
|
|
||||||
|
### **Performance Optimization:**
|
||||||
|
|
||||||
|
1. **Frontend:**
|
||||||
|
- Enable Vercel's edge caching
|
||||||
|
- Optimize bundle size
|
||||||
|
- Use CDN for static assets
|
||||||
|
|
||||||
|
2. **Backend:**
|
||||||
|
- Implement connection pooling
|
||||||
|
- Add caching layers
|
||||||
|
- Monitor memory usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 **Monitoring & Maintenance**
|
||||||
|
|
||||||
|
### **Health Checks:**
|
||||||
|
- Frontend: `https://your-app.vercel.app/health`
|
||||||
|
- Backend: `https://your-backend.railway.app/health`
|
||||||
|
|
||||||
|
### **Logs:**
|
||||||
|
- Vercel: Built-in function logs
|
||||||
|
- Railway: `railway logs`
|
||||||
|
- Render: Dashboard logs section
|
||||||
|
|
||||||
|
### **Scaling:**
|
||||||
|
- Vercel: Automatic scaling
|
||||||
|
- Railway: Manual scaling in dashboard
|
||||||
|
- Render: Auto-scaling available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Recommended Architecture**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Frontend │ │ Backend │ │ Database │
|
||||||
|
│ (Vercel) │◄──►│ (Railway) │◄──►│ (PostgreSQL) │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • React App │ │ • FastAPI │ │ • User Data │
|
||||||
|
│ • Static Files │ │ • AI Services │ │ • Content │
|
||||||
|
│ • CDN Cached │ │ • OAuth APIs │ │ • Analytics │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
This setup provides:
|
||||||
|
- ✅ **Fast frontend delivery** via Vercel's global CDN
|
||||||
|
- ✅ **Scalable backend** with Railway's infrastructure
|
||||||
|
- ✅ **Reliable database** with PostgreSQL
|
||||||
|
- ✅ **Easy maintenance** with separate concerns
|
||||||
|
- ✅ **Cost-effective** scaling based on usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 **Next Steps**
|
||||||
|
|
||||||
|
1. **Deploy backend** to Railway/Render
|
||||||
|
2. **Deploy frontend** to Vercel
|
||||||
|
3. **Configure environment variables**
|
||||||
|
4. **Test all integrations**
|
||||||
|
5. **Set up monitoring**
|
||||||
|
6. **Configure custom domain** (optional)
|
||||||
|
|
||||||
|
Your ALwrity platform will be live and accessible worldwide! 🌍
|
||||||
26
backend/alwrity_utils/__init__.py
Normal file
26
backend/alwrity_utils/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
ALwrity Utilities Package
|
||||||
|
Modular utilities for ALwrity backend startup and configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .dependency_manager import DependencyManager
|
||||||
|
from .environment_setup import EnvironmentSetup
|
||||||
|
from .database_setup import DatabaseSetup
|
||||||
|
from .production_optimizer import ProductionOptimizer
|
||||||
|
from .health_checker import HealthChecker
|
||||||
|
from .rate_limiter import RateLimiter
|
||||||
|
from .frontend_serving import FrontendServing
|
||||||
|
from .router_manager import RouterManager
|
||||||
|
from .onboarding_manager import OnboardingManager
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DependencyManager',
|
||||||
|
'EnvironmentSetup',
|
||||||
|
'DatabaseSetup',
|
||||||
|
'ProductionOptimizer',
|
||||||
|
'HealthChecker',
|
||||||
|
'RateLimiter',
|
||||||
|
'FrontendServing',
|
||||||
|
'RouterManager',
|
||||||
|
'OnboardingManager'
|
||||||
|
]
|
||||||
172
backend/alwrity_utils/database_setup.py
Normal file
172
backend/alwrity_utils/database_setup.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Database Setup Module
|
||||||
|
Handles database initialization and table creation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseSetup:
|
||||||
|
"""Manages database setup for ALwrity backend."""
|
||||||
|
|
||||||
|
def __init__(self, production_mode: bool = False):
|
||||||
|
self.production_mode = production_mode
|
||||||
|
|
||||||
|
def setup_essential_tables(self) -> bool:
|
||||||
|
"""Set up essential database tables."""
|
||||||
|
print("📊 Setting up essential database tables...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from services.database import init_database, engine
|
||||||
|
|
||||||
|
# Initialize database connection
|
||||||
|
init_database()
|
||||||
|
print(" ✅ Database connection initialized")
|
||||||
|
|
||||||
|
# Create essential tables
|
||||||
|
self._create_monitoring_tables()
|
||||||
|
self._create_subscription_tables()
|
||||||
|
self._create_persona_tables()
|
||||||
|
|
||||||
|
print("✅ Essential database tables created")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Warning: Database setup failed: {e}")
|
||||||
|
if self.production_mode:
|
||||||
|
print(" Continuing in production mode...")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" This may affect functionality")
|
||||||
|
return True # Don't fail startup for database issues
|
||||||
|
|
||||||
|
def _create_monitoring_tables(self) -> bool:
|
||||||
|
"""Create API monitoring tables."""
|
||||||
|
try:
|
||||||
|
from models.api_monitoring import Base as MonitoringBase
|
||||||
|
MonitoringBase.metadata.create_all(bind=engine)
|
||||||
|
print(" ✅ Monitoring tables created")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Monitoring tables failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def _create_subscription_tables(self) -> bool:
|
||||||
|
"""Create subscription and billing tables."""
|
||||||
|
try:
|
||||||
|
from models.subscription_models import Base as SubscriptionBase
|
||||||
|
SubscriptionBase.metadata.create_all(bind=engine)
|
||||||
|
print(" ✅ Subscription tables created")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Subscription tables failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def _create_persona_tables(self) -> bool:
|
||||||
|
"""Create persona analysis tables."""
|
||||||
|
try:
|
||||||
|
from models.persona_models import Base as PersonaBase
|
||||||
|
PersonaBase.metadata.create_all(bind=engine)
|
||||||
|
print(" ✅ Persona tables created")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Persona tables failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def verify_tables(self) -> bool:
|
||||||
|
"""Verify that essential tables exist."""
|
||||||
|
if self.production_mode:
|
||||||
|
print("⚠️ Skipping table verification in production mode")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("🔍 Verifying database tables...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from services.database import engine
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
inspector = inspect(engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
essential_tables = [
|
||||||
|
'api_monitoring_logs',
|
||||||
|
'subscription_plans',
|
||||||
|
'user_subscriptions'
|
||||||
|
]
|
||||||
|
|
||||||
|
existing_tables = [table for table in essential_tables if table in tables]
|
||||||
|
print(f" ✅ Found tables: {existing_tables}")
|
||||||
|
|
||||||
|
if len(existing_tables) < len(essential_tables):
|
||||||
|
missing = [table for table in essential_tables if table not in existing_tables]
|
||||||
|
print(f" ⚠️ Missing tables: {missing}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Table verification failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def setup_advanced_tables(self) -> bool:
|
||||||
|
"""Set up advanced tables (non-critical)."""
|
||||||
|
if self.production_mode:
|
||||||
|
print("⚠️ Skipping advanced table setup in production mode")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("🔧 Setting up advanced database features...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set up monitoring tables
|
||||||
|
self._setup_monitoring_tables()
|
||||||
|
|
||||||
|
# Set up billing tables
|
||||||
|
self._setup_billing_tables()
|
||||||
|
|
||||||
|
print("✅ Advanced database features configured")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Advanced table setup failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def _setup_monitoring_tables(self) -> bool:
|
||||||
|
"""Set up API monitoring tables."""
|
||||||
|
try:
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
from scripts.create_monitoring_tables import create_monitoring_tables
|
||||||
|
|
||||||
|
if create_monitoring_tables():
|
||||||
|
print(" ✅ API monitoring tables created")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ⚠️ API monitoring setup failed")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Monitoring setup failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
def _setup_billing_tables(self) -> bool:
|
||||||
|
"""Set up billing and subscription tables."""
|
||||||
|
try:
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
from scripts.create_billing_tables import create_billing_tables, check_existing_tables
|
||||||
|
from services.database import engine
|
||||||
|
|
||||||
|
# Check if tables already exist
|
||||||
|
if check_existing_tables(engine):
|
||||||
|
print(" ✅ Billing tables already exist")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if create_billing_tables():
|
||||||
|
print(" ✅ Billing tables created")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ⚠️ Billing setup failed")
|
||||||
|
return True # Non-critical
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Billing setup failed: {e}")
|
||||||
|
return True # Non-critical
|
||||||
150
backend/alwrity_utils/dependency_manager.py
Normal file
150
backend/alwrity_utils/dependency_manager.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
Dependency Management Module
|
||||||
|
Handles installation and verification of Python dependencies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyManager:
|
||||||
|
"""Manages Python package dependencies for ALwrity backend."""
|
||||||
|
|
||||||
|
def __init__(self, requirements_file: str = "requirements.txt"):
|
||||||
|
self.requirements_file = Path(requirements_file)
|
||||||
|
self.critical_packages = [
|
||||||
|
'fastapi',
|
||||||
|
'uvicorn',
|
||||||
|
'pydantic',
|
||||||
|
'sqlalchemy',
|
||||||
|
'loguru'
|
||||||
|
]
|
||||||
|
|
||||||
|
self.optional_packages = [
|
||||||
|
'openai',
|
||||||
|
'google.generativeai',
|
||||||
|
'anthropic',
|
||||||
|
'mistralai',
|
||||||
|
'spacy',
|
||||||
|
'nltk'
|
||||||
|
]
|
||||||
|
|
||||||
|
def install_requirements(self) -> bool:
|
||||||
|
"""Install packages from requirements.txt."""
|
||||||
|
print("📦 Installing required packages...")
|
||||||
|
|
||||||
|
if not self.requirements_file.exists():
|
||||||
|
print(f"❌ Requirements file not found: {self.requirements_file}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call([
|
||||||
|
sys.executable, "-m", "pip", "install", "-r", str(self.requirements_file)
|
||||||
|
])
|
||||||
|
print("✅ All packages installed successfully!")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Error installing packages: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_critical_dependencies(self) -> Tuple[bool, List[str]]:
|
||||||
|
"""Check if critical dependencies are available."""
|
||||||
|
print("🔍 Checking critical dependencies...")
|
||||||
|
|
||||||
|
missing_packages = []
|
||||||
|
|
||||||
|
for package in self.critical_packages:
|
||||||
|
try:
|
||||||
|
__import__(package.replace('-', '_'))
|
||||||
|
print(f" ✅ {package}")
|
||||||
|
except ImportError:
|
||||||
|
print(f" ❌ {package} - MISSING")
|
||||||
|
missing_packages.append(package)
|
||||||
|
|
||||||
|
if missing_packages:
|
||||||
|
print(f"❌ Missing critical packages: {', '.join(missing_packages)}")
|
||||||
|
return False, missing_packages
|
||||||
|
|
||||||
|
print("✅ All critical dependencies available!")
|
||||||
|
return True, []
|
||||||
|
|
||||||
|
def check_optional_dependencies(self) -> Tuple[bool, List[str]]:
|
||||||
|
"""Check if optional dependencies are available."""
|
||||||
|
print("🔍 Checking optional dependencies...")
|
||||||
|
|
||||||
|
missing_packages = []
|
||||||
|
|
||||||
|
for package in self.optional_packages:
|
||||||
|
try:
|
||||||
|
__import__(package.replace('-', '_'))
|
||||||
|
print(f" ✅ {package}")
|
||||||
|
except ImportError:
|
||||||
|
print(f" ⚠️ {package} - MISSING (optional)")
|
||||||
|
missing_packages.append(package)
|
||||||
|
|
||||||
|
if missing_packages:
|
||||||
|
print(f"⚠️ Missing optional packages: {', '.join(missing_packages)}")
|
||||||
|
print(" Some features may not be available")
|
||||||
|
|
||||||
|
return len(missing_packages) == 0, missing_packages
|
||||||
|
|
||||||
|
def setup_spacy_model(self) -> bool:
|
||||||
|
"""Set up spaCy English model (production-optimized)."""
|
||||||
|
print("🧠 Setting up spaCy model...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import spacy
|
||||||
|
|
||||||
|
model_name = "en_core_web_sm"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to load the model
|
||||||
|
nlp = spacy.load(model_name)
|
||||||
|
test_doc = nlp("This is a test sentence.")
|
||||||
|
if test_doc and len(test_doc) > 0:
|
||||||
|
print(f"✅ spaCy model '{model_name}' is available")
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
print(f"⚠️ spaCy model '{model_name}' not found")
|
||||||
|
print(" Skipping spaCy setup in production mode")
|
||||||
|
return True # Don't fail for missing spaCy in production
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ spaCy not installed - skipping model setup")
|
||||||
|
return True # Don't fail for missing spaCy
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setup_nltk_data(self) -> bool:
|
||||||
|
"""Set up NLTK data (production-optimized)."""
|
||||||
|
print("📚 Setting up NLTK data...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nltk
|
||||||
|
|
||||||
|
# Only download essential data
|
||||||
|
essential_data = ['punkt', 'stopwords']
|
||||||
|
|
||||||
|
for data_package in essential_data:
|
||||||
|
try:
|
||||||
|
nltk.data.find(f'tokenizers/{data_package}' if data_package == 'punkt'
|
||||||
|
else f'corpora/{data_package}')
|
||||||
|
print(f" ✅ {data_package}")
|
||||||
|
except LookupError:
|
||||||
|
print(f" ⚠️ {data_package} - downloading...")
|
||||||
|
try:
|
||||||
|
nltk.download(data_package, quiet=True)
|
||||||
|
print(f" ✅ {data_package} downloaded")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ {data_package} download failed: {e}")
|
||||||
|
|
||||||
|
print("✅ NLTK data setup complete")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ NLTK not installed - skipping data setup")
|
||||||
|
return True # Don't fail for missing NLTK
|
||||||
|
|
||||||
|
return True
|
||||||
147
backend/alwrity_utils/environment_setup.py
Normal file
147
backend/alwrity_utils/environment_setup.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Environment Setup Module
|
||||||
|
Handles environment configuration and directory setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentSetup:
|
||||||
|
"""Manages environment setup for ALwrity backend."""
|
||||||
|
|
||||||
|
def __init__(self, production_mode: bool = False):
|
||||||
|
self.production_mode = production_mode
|
||||||
|
# Use safer directory paths that don't conflict with deployment platforms
|
||||||
|
if production_mode:
|
||||||
|
# In production, use temp directories or skip directory creation
|
||||||
|
self.required_directories = []
|
||||||
|
else:
|
||||||
|
# In development, use local directories
|
||||||
|
self.required_directories = [
|
||||||
|
"lib/workspace/alwrity_content",
|
||||||
|
"lib/workspace/alwrity_web_research",
|
||||||
|
"lib/workspace/alwrity_prompts",
|
||||||
|
"lib/workspace/alwrity_config"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_directories(self) -> bool:
|
||||||
|
"""Create necessary directories for ALwrity."""
|
||||||
|
print("📁 Setting up directories...")
|
||||||
|
|
||||||
|
if not self.required_directories:
|
||||||
|
print(" ⚠️ Skipping directory creation in production mode")
|
||||||
|
return True
|
||||||
|
|
||||||
|
for directory in self.required_directories:
|
||||||
|
try:
|
||||||
|
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||||
|
print(f" ✅ Created: {directory}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Failed to create {directory}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ All directories created successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setup_environment_variables(self) -> bool:
|
||||||
|
"""Set up environment variables for the application."""
|
||||||
|
print("🔧 Setting up environment variables...")
|
||||||
|
|
||||||
|
# Production environment variables
|
||||||
|
if self.production_mode:
|
||||||
|
env_vars = {
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": "8000",
|
||||||
|
"RELOAD": "false",
|
||||||
|
"LOG_LEVEL": "INFO",
|
||||||
|
"DEBUG": "false"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
env_vars = {
|
||||||
|
"HOST": "0.0.0.0",
|
||||||
|
"PORT": "8000",
|
||||||
|
"RELOAD": "true",
|
||||||
|
"LOG_LEVEL": "DEBUG",
|
||||||
|
"DEBUG": "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in env_vars.items():
|
||||||
|
os.environ.setdefault(key, value)
|
||||||
|
print(f" ✅ {key}={value}")
|
||||||
|
|
||||||
|
print("✅ Environment variables configured")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_env_file(self) -> bool:
|
||||||
|
"""Create .env file with default configuration (development only)."""
|
||||||
|
if self.production_mode:
|
||||||
|
print("⚠️ Skipping .env file creation in production mode")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("🔧 Creating .env file...")
|
||||||
|
|
||||||
|
env_file = Path(".env")
|
||||||
|
if env_file.exists():
|
||||||
|
print(" ✅ .env file already exists")
|
||||||
|
return True
|
||||||
|
|
||||||
|
env_content = """# ALwrity Backend Configuration
|
||||||
|
|
||||||
|
# API Keys (Configure these in the onboarding process)
|
||||||
|
# OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
# GEMINI_API_KEY=your_gemini_api_key_here
|
||||||
|
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
# MISTRAL_API_KEY=your_mistral_api_key_here
|
||||||
|
|
||||||
|
# Research API Keys (Optional)
|
||||||
|
# TAVILY_API_KEY=your_tavily_api_key_here
|
||||||
|
# SERPER_API_KEY=your_serper_api_key_here
|
||||||
|
# EXA_API_KEY=your_exa_api_key_here
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
# CLERK_SECRET_KEY=your_clerk_secret_key_here
|
||||||
|
|
||||||
|
# OAuth Redirect URIs
|
||||||
|
# GSC_REDIRECT_URI=https://your-frontend.vercel.app/gsc/callback
|
||||||
|
# WORDPRESS_REDIRECT_URI=https://your-frontend.vercel.app/wp/callback
|
||||||
|
# WIX_REDIRECT_URI=https://your-frontend.vercel.app/wix/callback
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
|
DEBUG=true
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(env_file, 'w') as f:
|
||||||
|
f.write(env_content)
|
||||||
|
print("✅ .env file created successfully")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating .env file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def verify_environment(self) -> bool:
|
||||||
|
"""Verify that the environment is properly configured."""
|
||||||
|
print("🔍 Verifying environment setup...")
|
||||||
|
|
||||||
|
# Check required directories
|
||||||
|
for directory in self.required_directories:
|
||||||
|
if not Path(directory).exists():
|
||||||
|
print(f"❌ Directory missing: {directory}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
required_vars = ["HOST", "PORT", "LOG_LEVEL"]
|
||||||
|
for var in required_vars:
|
||||||
|
if not os.getenv(var):
|
||||||
|
print(f"❌ Environment variable missing: {var}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Environment verification complete")
|
||||||
|
return True
|
||||||
82
backend/alwrity_utils/frontend_serving.py
Normal file
82
backend/alwrity_utils/frontend_serving.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Frontend Serving Module
|
||||||
|
Handles React frontend serving and static file mounting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from loguru import logger
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendServing:
|
||||||
|
"""Manages React frontend serving and static file mounting."""
|
||||||
|
|
||||||
|
def __init__(self, app: FastAPI):
|
||||||
|
self.app = app
|
||||||
|
self.frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "build")
|
||||||
|
self.static_path = os.path.join(self.frontend_build_path, "static")
|
||||||
|
|
||||||
|
def setup_frontend_serving(self) -> bool:
|
||||||
|
"""Set up React frontend serving and static file mounting."""
|
||||||
|
try:
|
||||||
|
logger.info("Setting up frontend serving...")
|
||||||
|
|
||||||
|
# Mount static files for React app (only if directory exists)
|
||||||
|
if os.path.exists(self.static_path):
|
||||||
|
self.app.mount("/static", StaticFiles(directory=self.static_path), name="static")
|
||||||
|
logger.info("Frontend static files mounted successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info("Frontend build directory not found. Static files not mounted.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not mount static files: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def serve_frontend(self) -> FileResponse | Dict[str, Any]:
|
||||||
|
"""Serve the React frontend."""
|
||||||
|
try:
|
||||||
|
# Check if frontend build exists
|
||||||
|
index_html = os.path.join(self.frontend_build_path, "index.html")
|
||||||
|
|
||||||
|
if os.path.exists(index_html):
|
||||||
|
return FileResponse(index_html)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"message": "Frontend not built. Please run 'npm run build' in the frontend directory.",
|
||||||
|
"api_docs": "/api/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error serving frontend: {e}")
|
||||||
|
return {
|
||||||
|
"message": "Error serving frontend",
|
||||||
|
"error": str(e),
|
||||||
|
"api_docs": "/api/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_frontend_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get the status of frontend build and serving."""
|
||||||
|
try:
|
||||||
|
index_html = os.path.join(self.frontend_build_path, "index.html")
|
||||||
|
static_exists = os.path.exists(self.static_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"frontend_build_path": self.frontend_build_path,
|
||||||
|
"static_path": self.static_path,
|
||||||
|
"index_html_exists": os.path.exists(index_html),
|
||||||
|
"static_files_exist": static_exists,
|
||||||
|
"frontend_ready": os.path.exists(index_html) and static_exists
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking frontend status: {e}")
|
||||||
|
return {
|
||||||
|
"error": str(e),
|
||||||
|
"frontend_ready": False
|
||||||
|
}
|
||||||
129
backend/alwrity_utils/health_checker.py
Normal file
129
backend/alwrity_utils/health_checker.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
Health Check Module
|
||||||
|
Handles health check endpoints and database health verification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class HealthChecker:
|
||||||
|
"""Manages health check functionality for ALwrity backend."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.startup_time = datetime.utcnow()
|
||||||
|
|
||||||
|
def basic_health_check(self) -> Dict[str, Any]:
|
||||||
|
"""Basic health check endpoint."""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"message": "ALwrity backend is running",
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"uptime": str(datetime.utcnow() - self.startup_time)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health check failed: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Health check failed: {str(e)}",
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def database_health_check(self) -> Dict[str, Any]:
|
||||||
|
"""Database health check endpoint including persona tables verification."""
|
||||||
|
try:
|
||||||
|
from services.database import get_db_session
|
||||||
|
from models.persona_models import (
|
||||||
|
WritingPersona,
|
||||||
|
PlatformPersona,
|
||||||
|
PersonaAnalysisResult,
|
||||||
|
PersonaValidationResult
|
||||||
|
)
|
||||||
|
|
||||||
|
session = get_db_session()
|
||||||
|
if not session:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Could not get database session",
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test all persona tables
|
||||||
|
tables_status = {}
|
||||||
|
try:
|
||||||
|
session.query(WritingPersona).first()
|
||||||
|
tables_status["writing_personas"] = "ok"
|
||||||
|
except Exception as e:
|
||||||
|
tables_status["writing_personas"] = f"error: {str(e)}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.query(PlatformPersona).first()
|
||||||
|
tables_status["platform_personas"] = "ok"
|
||||||
|
except Exception as e:
|
||||||
|
tables_status["platform_personas"] = f"error: {str(e)}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.query(PersonaAnalysisResult).first()
|
||||||
|
tables_status["persona_analysis_results"] = "ok"
|
||||||
|
except Exception as e:
|
||||||
|
tables_status["persona_analysis_results"] = f"error: {str(e)}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.query(PersonaValidationResult).first()
|
||||||
|
tables_status["persona_validation_results"] = "ok"
|
||||||
|
except Exception as e:
|
||||||
|
tables_status["persona_validation_results"] = f"error: {str(e)}"
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
# Check if all tables are ok
|
||||||
|
all_ok = all(status == "ok" for status in tables_status.values())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "healthy" if all_ok else "warning",
|
||||||
|
"message": "Database connection successful" if all_ok else "Some persona tables may have issues",
|
||||||
|
"persona_tables": tables_status,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database health check failed: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Database health check failed: {str(e)}",
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def comprehensive_health_check(self) -> Dict[str, Any]:
|
||||||
|
"""Comprehensive health check including all services."""
|
||||||
|
try:
|
||||||
|
# Basic health
|
||||||
|
basic_health = self.basic_health_check()
|
||||||
|
|
||||||
|
# Database health
|
||||||
|
db_health = self.database_health_check()
|
||||||
|
|
||||||
|
# Determine overall status
|
||||||
|
overall_status = "healthy"
|
||||||
|
if basic_health["status"] != "healthy" or db_health["status"] == "error":
|
||||||
|
overall_status = "unhealthy"
|
||||||
|
elif db_health["status"] == "warning":
|
||||||
|
overall_status = "degraded"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": overall_status,
|
||||||
|
"basic": basic_health,
|
||||||
|
"database": db_health,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Comprehensive health check failed: {e}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Comprehensive health check failed: {str(e)}",
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
455
backend/alwrity_utils/onboarding_manager.py
Normal file
455
backend/alwrity_utils/onboarding_manager.py
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
"""
|
||||||
|
Onboarding Manager Module
|
||||||
|
Handles all onboarding-related endpoints and functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# Import onboarding functions
|
||||||
|
from api.onboarding import (
|
||||||
|
health_check,
|
||||||
|
initialize_onboarding,
|
||||||
|
get_onboarding_status,
|
||||||
|
get_onboarding_progress_full,
|
||||||
|
get_step_data,
|
||||||
|
complete_step,
|
||||||
|
skip_step,
|
||||||
|
validate_step_access,
|
||||||
|
get_api_keys,
|
||||||
|
get_api_keys_for_onboarding,
|
||||||
|
save_api_key,
|
||||||
|
validate_api_keys,
|
||||||
|
start_onboarding,
|
||||||
|
complete_onboarding,
|
||||||
|
reset_onboarding,
|
||||||
|
get_resume_info,
|
||||||
|
get_onboarding_config,
|
||||||
|
get_provider_setup_info,
|
||||||
|
get_all_providers_info,
|
||||||
|
validate_provider_key,
|
||||||
|
get_enhanced_validation_status,
|
||||||
|
get_onboarding_summary,
|
||||||
|
get_website_analysis_data,
|
||||||
|
get_research_preferences_data,
|
||||||
|
save_business_info,
|
||||||
|
get_business_info,
|
||||||
|
get_business_info_by_user,
|
||||||
|
update_business_info,
|
||||||
|
generate_writing_personas,
|
||||||
|
generate_writing_personas_async,
|
||||||
|
get_persona_task_status,
|
||||||
|
assess_persona_quality,
|
||||||
|
regenerate_persona,
|
||||||
|
get_persona_generation_options,
|
||||||
|
get_latest_persona,
|
||||||
|
save_persona_update,
|
||||||
|
StepCompletionRequest,
|
||||||
|
APIKeyRequest
|
||||||
|
)
|
||||||
|
from middleware.auth_middleware import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
class OnboardingManager:
|
||||||
|
"""Manages all onboarding-related endpoints and functionality."""
|
||||||
|
|
||||||
|
def __init__(self, app: FastAPI):
|
||||||
|
self.app = app
|
||||||
|
self.setup_onboarding_endpoints()
|
||||||
|
|
||||||
|
def setup_onboarding_endpoints(self):
|
||||||
|
"""Set up all onboarding-related endpoints."""
|
||||||
|
|
||||||
|
# Onboarding initialization - BATCH ENDPOINT (reduces 4 API calls to 1)
|
||||||
|
@self.app.get("/api/onboarding/init")
|
||||||
|
async def onboarding_init(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Batch initialization endpoint - combines user info, status, and progress.
|
||||||
|
This eliminates 3-4 separate API calls on initial load, reducing latency by 60-75%.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await initialize_onboarding(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_init: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Onboarding status endpoints
|
||||||
|
@self.app.get("/api/onboarding/status")
|
||||||
|
async def onboarding_status(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get the current onboarding status."""
|
||||||
|
try:
|
||||||
|
return await get_onboarding_status(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_status: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/progress")
|
||||||
|
async def onboarding_progress(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get the full onboarding progress data."""
|
||||||
|
try:
|
||||||
|
return await get_onboarding_progress_full(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_progress: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Step management endpoints
|
||||||
|
@self.app.get("/api/onboarding/step/{step_number}")
|
||||||
|
async def step_data(step_number: int, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get data for a specific step."""
|
||||||
|
try:
|
||||||
|
return await get_step_data(step_number, current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in step_data: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step/{step_number}/complete")
|
||||||
|
async def step_complete(step_number: int, request: StepCompletionRequest, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Mark a step as completed."""
|
||||||
|
try:
|
||||||
|
return await complete_step(step_number, request, current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in step_complete: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step/{step_number}/skip")
|
||||||
|
async def step_skip(step_number: int, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Skip a step (for optional steps)."""
|
||||||
|
try:
|
||||||
|
return await skip_step(step_number, current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in step_skip: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/step/{step_number}/validate")
|
||||||
|
async def step_validate(step_number: int, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Validate if user can access a specific step."""
|
||||||
|
try:
|
||||||
|
return await validate_step_access(step_number, current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in step_validate: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# API key management endpoints
|
||||||
|
@self.app.get("/api/onboarding/api-keys")
|
||||||
|
async def api_keys():
|
||||||
|
"""Get all configured API keys (masked)."""
|
||||||
|
try:
|
||||||
|
return await get_api_keys()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in api_keys: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/api-keys/onboarding")
|
||||||
|
async def api_keys_for_onboarding():
|
||||||
|
"""Get all configured API keys for onboarding (unmasked)."""
|
||||||
|
try:
|
||||||
|
return await get_api_keys_for_onboarding()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in api_keys_for_onboarding: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/api-keys")
|
||||||
|
async def api_key_save(request: APIKeyRequest):
|
||||||
|
"""Save an API key for a provider."""
|
||||||
|
try:
|
||||||
|
return await save_api_key(request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in api_key_save: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/api-keys/validate")
|
||||||
|
async def api_key_validate():
|
||||||
|
"""Validate all configured API keys."""
|
||||||
|
try:
|
||||||
|
return await validate_api_keys()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in api_key_validate: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Onboarding control endpoints
|
||||||
|
@self.app.post("/api/onboarding/start")
|
||||||
|
async def onboarding_start(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Start a new onboarding session."""
|
||||||
|
try:
|
||||||
|
return await start_onboarding(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_start: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/complete")
|
||||||
|
async def onboarding_complete(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Complete the onboarding process."""
|
||||||
|
try:
|
||||||
|
return await complete_onboarding(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_complete: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/reset")
|
||||||
|
async def onboarding_reset():
|
||||||
|
"""Reset the onboarding progress."""
|
||||||
|
try:
|
||||||
|
return await reset_onboarding()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_reset: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Resume functionality
|
||||||
|
@self.app.get("/api/onboarding/resume")
|
||||||
|
async def onboarding_resume():
|
||||||
|
"""Get information for resuming onboarding."""
|
||||||
|
try:
|
||||||
|
return await get_resume_info()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_resume: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Configuration endpoints
|
||||||
|
@self.app.get("/api/onboarding/config")
|
||||||
|
async def onboarding_config():
|
||||||
|
"""Get onboarding configuration and requirements."""
|
||||||
|
try:
|
||||||
|
return get_onboarding_config()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_config: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Enhanced provider endpoints
|
||||||
|
@self.app.get("/api/onboarding/providers/{provider}/setup")
|
||||||
|
async def provider_setup_info(provider: str):
|
||||||
|
"""Get setup information for a specific provider."""
|
||||||
|
try:
|
||||||
|
return await get_provider_setup_info(provider)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in provider_setup_info: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/providers")
|
||||||
|
async def all_providers_info():
|
||||||
|
"""Get setup information for all providers."""
|
||||||
|
try:
|
||||||
|
return await get_all_providers_info()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in all_providers_info: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/providers/{provider}/validate")
|
||||||
|
async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest):
|
||||||
|
"""Validate a specific provider's API key."""
|
||||||
|
try:
|
||||||
|
return await validate_provider_key(provider, request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in validate_provider_key: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/validation/enhanced")
|
||||||
|
async def enhanced_validation_status():
|
||||||
|
"""Get enhanced validation status for all configured services."""
|
||||||
|
try:
|
||||||
|
return await get_enhanced_validation_status()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in enhanced_validation_status: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# New endpoints for FinalStep data loading
|
||||||
|
@self.app.get("/api/onboarding/summary")
|
||||||
|
async def onboarding_summary(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get comprehensive onboarding summary for FinalStep."""
|
||||||
|
try:
|
||||||
|
return await get_onboarding_summary(current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in onboarding_summary: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/website-analysis")
|
||||||
|
async def website_analysis_data(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get website analysis data for FinalStep."""
|
||||||
|
try:
|
||||||
|
return await get_website_analysis_data(current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in website_analysis_data: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/research-preferences")
|
||||||
|
async def research_preferences_data(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get research preferences data for FinalStep."""
|
||||||
|
try:
|
||||||
|
return await get_research_preferences_data(current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in research_preferences_data: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Business Information endpoints
|
||||||
|
@self.app.post("/api/onboarding/business-info")
|
||||||
|
async def business_info_save(request: 'BusinessInfoRequest'):
|
||||||
|
"""Save business information for users without websites."""
|
||||||
|
try:
|
||||||
|
from models.business_info_request import BusinessInfoRequest
|
||||||
|
return await save_business_info(request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in business_info_save: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/business-info/{business_info_id}")
|
||||||
|
async def business_info_get(business_info_id: int):
|
||||||
|
"""Get business information by ID."""
|
||||||
|
try:
|
||||||
|
return await get_business_info(business_info_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in business_info_get: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/business-info/user/{user_id}")
|
||||||
|
async def business_info_get_by_user(user_id: int):
|
||||||
|
"""Get business information by user ID."""
|
||||||
|
try:
|
||||||
|
return await get_business_info_by_user(user_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in business_info_get_by_user: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.put("/api/onboarding/business-info/{business_info_id}")
|
||||||
|
async def business_info_update(business_info_id: int, request: 'BusinessInfoRequest'):
|
||||||
|
"""Update business information."""
|
||||||
|
try:
|
||||||
|
from models.business_info_request import BusinessInfoRequest
|
||||||
|
return await update_business_info(business_info_id, request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in business_info_update: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Persona generation endpoints
|
||||||
|
@self.app.post("/api/onboarding/step4/generate-personas")
|
||||||
|
async def generate_personas(request: dict, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Generate AI writing personas for Step 4."""
|
||||||
|
try:
|
||||||
|
return await generate_writing_personas(request, current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in generate_personas: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step4/generate-personas-async")
|
||||||
|
async def generate_personas_async(request: dict, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Start async persona generation task."""
|
||||||
|
try:
|
||||||
|
return await generate_writing_personas_async(request, current_user, background_tasks)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in generate_personas_async: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/step4/persona-task/{task_id}")
|
||||||
|
async def get_persona_task(task_id: str):
|
||||||
|
"""Get persona generation task status."""
|
||||||
|
try:
|
||||||
|
return await get_persona_task_status(task_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_persona_task: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/step4/persona-latest")
|
||||||
|
async def persona_latest(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get latest cached persona for current user."""
|
||||||
|
try:
|
||||||
|
return await get_latest_persona(current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in persona_latest: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step4/persona-save")
|
||||||
|
async def persona_save(request: dict, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Save edited persona back to cache."""
|
||||||
|
try:
|
||||||
|
return await save_persona_update(request, current_user)
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in persona_save: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step4/assess-persona-quality")
|
||||||
|
async def assess_persona_quality_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Assess the quality of generated personas."""
|
||||||
|
try:
|
||||||
|
return await assess_persona_quality(request, current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in assess_persona_quality: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.post("/api/onboarding/step4/regenerate-persona")
|
||||||
|
async def regenerate_persona_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Regenerate a specific persona with improvements."""
|
||||||
|
try:
|
||||||
|
return await regenerate_persona(request, current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in regenerate_persona: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@self.app.get("/api/onboarding/step4/persona-options")
|
||||||
|
async def get_persona_options(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get persona generation options and configurations."""
|
||||||
|
try:
|
||||||
|
return await get_persona_generation_options(current_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_persona_options: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
def get_onboarding_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get the status of onboarding endpoints."""
|
||||||
|
return {
|
||||||
|
"onboarding_endpoints": [
|
||||||
|
"/api/onboarding/init",
|
||||||
|
"/api/onboarding/status",
|
||||||
|
"/api/onboarding/progress",
|
||||||
|
"/api/onboarding/step/{step_number}",
|
||||||
|
"/api/onboarding/step/{step_number}/complete",
|
||||||
|
"/api/onboarding/step/{step_number}/skip",
|
||||||
|
"/api/onboarding/step/{step_number}/validate",
|
||||||
|
"/api/onboarding/api-keys",
|
||||||
|
"/api/onboarding/api-keys/onboarding",
|
||||||
|
"/api/onboarding/start",
|
||||||
|
"/api/onboarding/complete",
|
||||||
|
"/api/onboarding/reset",
|
||||||
|
"/api/onboarding/resume",
|
||||||
|
"/api/onboarding/config",
|
||||||
|
"/api/onboarding/providers/{provider}/setup",
|
||||||
|
"/api/onboarding/providers",
|
||||||
|
"/api/onboarding/providers/{provider}/validate",
|
||||||
|
"/api/onboarding/validation/enhanced",
|
||||||
|
"/api/onboarding/summary",
|
||||||
|
"/api/onboarding/website-analysis",
|
||||||
|
"/api/onboarding/research-preferences",
|
||||||
|
"/api/onboarding/business-info",
|
||||||
|
"/api/onboarding/step4/generate-personas",
|
||||||
|
"/api/onboarding/step4/generate-personas-async",
|
||||||
|
"/api/onboarding/step4/persona-task/{task_id}",
|
||||||
|
"/api/onboarding/step4/persona-latest",
|
||||||
|
"/api/onboarding/step4/persona-save",
|
||||||
|
"/api/onboarding/step4/assess-persona-quality",
|
||||||
|
"/api/onboarding/step4/regenerate-persona",
|
||||||
|
"/api/onboarding/step4/persona-options"
|
||||||
|
],
|
||||||
|
"total_endpoints": 30,
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
137
backend/alwrity_utils/production_optimizer.py
Normal file
137
backend/alwrity_utils/production_optimizer.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
Production Optimizer Module
|
||||||
|
Handles production-specific optimizations and configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionOptimizer:
|
||||||
|
"""Optimizes ALwrity backend for production deployment."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.production_optimizations = {
|
||||||
|
'disable_spacy_download': True,
|
||||||
|
'disable_nltk_download': True,
|
||||||
|
'skip_linguistic_setup': True,
|
||||||
|
'minimal_database_setup': True,
|
||||||
|
'skip_file_creation': True
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply_production_optimizations(self) -> bool:
|
||||||
|
"""Apply production-specific optimizations."""
|
||||||
|
print("🚀 Applying production optimizations...")
|
||||||
|
|
||||||
|
# Set production environment variables
|
||||||
|
self._set_production_env_vars()
|
||||||
|
|
||||||
|
# Disable heavy operations
|
||||||
|
self._disable_heavy_operations()
|
||||||
|
|
||||||
|
# Optimize logging
|
||||||
|
self._optimize_logging()
|
||||||
|
|
||||||
|
print("✅ Production optimizations applied")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _set_production_env_vars(self) -> None:
|
||||||
|
"""Set production-specific environment variables."""
|
||||||
|
production_vars = {
|
||||||
|
'HOST': '0.0.0.0',
|
||||||
|
'PORT': '8000',
|
||||||
|
'RELOAD': 'false',
|
||||||
|
'LOG_LEVEL': 'INFO',
|
||||||
|
'DEBUG': 'false',
|
||||||
|
'PYTHONUNBUFFERED': '1', # Ensure logs are flushed immediately
|
||||||
|
'PYTHONDONTWRITEBYTECODE': '1' # Don't create .pyc files
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in production_vars.items():
|
||||||
|
os.environ.setdefault(key, value)
|
||||||
|
print(f" ✅ {key}={value}")
|
||||||
|
|
||||||
|
def _disable_heavy_operations(self) -> None:
|
||||||
|
"""Disable operations that are too heavy for production startup."""
|
||||||
|
print(" ⚡ Disabling heavy operations for production...")
|
||||||
|
|
||||||
|
# Disable spaCy model download
|
||||||
|
os.environ.setdefault('DISABLE_SPACY_DOWNLOAD', 'true')
|
||||||
|
|
||||||
|
# Disable NLTK data download
|
||||||
|
os.environ.setdefault('DISABLE_NLTK_DOWNLOAD', 'true')
|
||||||
|
|
||||||
|
# Skip linguistic analyzer setup
|
||||||
|
os.environ.setdefault('SKIP_LINGUISTIC_SETUP', 'true')
|
||||||
|
|
||||||
|
print(" ✅ Heavy operations disabled")
|
||||||
|
|
||||||
|
def _optimize_logging(self) -> None:
|
||||||
|
"""Optimize logging for production."""
|
||||||
|
print(" 📝 Optimizing logging for production...")
|
||||||
|
|
||||||
|
# Set appropriate log level
|
||||||
|
os.environ.setdefault('LOG_LEVEL', 'INFO')
|
||||||
|
|
||||||
|
# Disable debug logging
|
||||||
|
os.environ.setdefault('DEBUG', 'false')
|
||||||
|
|
||||||
|
print(" ✅ Logging optimized")
|
||||||
|
|
||||||
|
def skip_linguistic_setup(self) -> bool:
|
||||||
|
"""Skip linguistic analysis setup in production."""
|
||||||
|
if os.getenv('SKIP_LINGUISTIC_SETUP', 'false').lower() == 'true':
|
||||||
|
print("⚠️ Skipping linguistic analysis setup (production mode)")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def skip_spacy_setup(self) -> bool:
|
||||||
|
"""Skip spaCy model setup in production."""
|
||||||
|
if os.getenv('DISABLE_SPACY_DOWNLOAD', 'false').lower() == 'true':
|
||||||
|
print("⚠️ Skipping spaCy model setup (production mode)")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def skip_nltk_setup(self) -> bool:
|
||||||
|
"""Skip NLTK data setup in production."""
|
||||||
|
if os.getenv('DISABLE_NLTK_DOWNLOAD', 'false').lower() == 'true':
|
||||||
|
print("⚠️ Skipping NLTK data setup (production mode)")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_production_config(self) -> Dict[str, Any]:
|
||||||
|
"""Get production configuration settings."""
|
||||||
|
return {
|
||||||
|
'host': os.getenv('HOST', '0.0.0.0'),
|
||||||
|
'port': int(os.getenv('PORT', '8000')),
|
||||||
|
'reload': False, # Never reload in production
|
||||||
|
'log_level': os.getenv('LOG_LEVEL', 'info'),
|
||||||
|
'access_log': True,
|
||||||
|
'workers': 1, # Single worker for Render
|
||||||
|
'timeout_keep_alive': 30,
|
||||||
|
'timeout_graceful_shutdown': 30
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_production_environment(self) -> bool:
|
||||||
|
"""Validate that the environment is ready for production."""
|
||||||
|
print("🔍 Validating production environment...")
|
||||||
|
|
||||||
|
# Check critical environment variables
|
||||||
|
required_vars = ['HOST', 'PORT', 'LOG_LEVEL']
|
||||||
|
missing_vars = []
|
||||||
|
|
||||||
|
for var in required_vars:
|
||||||
|
if not os.getenv(var):
|
||||||
|
missing_vars.append(var)
|
||||||
|
|
||||||
|
if missing_vars:
|
||||||
|
print(f"❌ Missing environment variables: {missing_vars}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check that reload is disabled
|
||||||
|
if os.getenv('RELOAD', 'false').lower() == 'true':
|
||||||
|
print("⚠️ Warning: RELOAD is enabled in production")
|
||||||
|
|
||||||
|
print("✅ Production environment validated")
|
||||||
|
return True
|
||||||
125
backend/alwrity_utils/rate_limiter.py
Normal file
125
backend/alwrity_utils/rate_limiter.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Rate Limiting Module
|
||||||
|
Handles rate limiting middleware and request tracking.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from fastapi import Request, Response
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
"""Manages rate limiting for ALwrity backend."""
|
||||||
|
|
||||||
|
def __init__(self, window_seconds: int = 60, max_requests: int = 200):
|
||||||
|
self.window_seconds = window_seconds
|
||||||
|
self.max_requests = max_requests
|
||||||
|
self.request_counts: Dict[str, List[float]] = defaultdict(list)
|
||||||
|
|
||||||
|
# Endpoints exempt from rate limiting
|
||||||
|
self.exempt_paths = [
|
||||||
|
"/stream/strategies",
|
||||||
|
"/stream/strategic-intelligence",
|
||||||
|
"/stream/keyword-research",
|
||||||
|
"/latest-strategy",
|
||||||
|
"/ai-analytics",
|
||||||
|
"/gap-analysis",
|
||||||
|
"/calendar-events",
|
||||||
|
"/calendar-generation/progress",
|
||||||
|
"/health",
|
||||||
|
"/health/database"
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_exempt_path(self, path: str) -> bool:
|
||||||
|
"""Check if a path is exempt from rate limiting."""
|
||||||
|
return any(exempt_path in path for exempt_path in self.exempt_paths)
|
||||||
|
|
||||||
|
def clean_old_requests(self, client_ip: str, current_time: float) -> None:
|
||||||
|
"""Clean old requests from the tracking dictionary."""
|
||||||
|
self.request_counts[client_ip] = [
|
||||||
|
req_time for req_time in self.request_counts[client_ip]
|
||||||
|
if current_time - req_time < self.window_seconds
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_rate_limited(self, client_ip: str, current_time: float) -> bool:
|
||||||
|
"""Check if a client has exceeded the rate limit."""
|
||||||
|
self.clean_old_requests(client_ip, current_time)
|
||||||
|
return len(self.request_counts[client_ip]) >= self.max_requests
|
||||||
|
|
||||||
|
def add_request(self, client_ip: str, current_time: float) -> None:
|
||||||
|
"""Add a request to the tracking dictionary."""
|
||||||
|
self.request_counts[client_ip].append(current_time)
|
||||||
|
|
||||||
|
def get_rate_limit_response(self) -> JSONResponse:
|
||||||
|
"""Get a rate limit exceeded response."""
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=429,
|
||||||
|
content={
|
||||||
|
"detail": "Too many requests",
|
||||||
|
"retry_after": self.window_seconds
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "*",
|
||||||
|
"Access-Control-Allow-Headers": "*"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def rate_limit_middleware(self, request: Request, call_next) -> Response:
|
||||||
|
"""Rate limiting middleware with exemptions for streaming endpoints."""
|
||||||
|
try:
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
current_time = time.time()
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
|
# Check if path is exempt from rate limiting
|
||||||
|
if self.is_exempt_path(path):
|
||||||
|
# Allow streaming endpoints without rate limiting
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Check rate limit
|
||||||
|
if self.is_rate_limited(client_ip, current_time):
|
||||||
|
logger.warning(f"Rate limit exceeded for {client_ip}")
|
||||||
|
return self.get_rate_limit_response()
|
||||||
|
|
||||||
|
# Add current request
|
||||||
|
self.add_request(client_ip, current_time)
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in rate limiting middleware: {e}")
|
||||||
|
# Continue without rate limiting if there's an error
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_rate_limit_status(self, client_ip: str) -> Dict[str, any]:
|
||||||
|
"""Get current rate limit status for a client."""
|
||||||
|
current_time = time.time()
|
||||||
|
self.clean_old_requests(client_ip, current_time)
|
||||||
|
|
||||||
|
request_count = len(self.request_counts[client_ip])
|
||||||
|
remaining_requests = max(0, self.max_requests - request_count)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"client_ip": client_ip,
|
||||||
|
"requests_in_window": request_count,
|
||||||
|
"max_requests": self.max_requests,
|
||||||
|
"remaining_requests": remaining_requests,
|
||||||
|
"window_seconds": self.window_seconds,
|
||||||
|
"is_limited": request_count >= self.max_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset_rate_limit(self, client_ip: Optional[str] = None) -> Dict[str, any]:
|
||||||
|
"""Reset rate limit for a specific client or all clients."""
|
||||||
|
if client_ip:
|
||||||
|
self.request_counts[client_ip] = []
|
||||||
|
return {"message": f"Rate limit reset for {client_ip}"}
|
||||||
|
else:
|
||||||
|
self.request_counts.clear()
|
||||||
|
return {"message": "Rate limit reset for all clients"}
|
||||||
168
backend/alwrity_utils/router_manager.py
Normal file
168
backend/alwrity_utils/router_manager.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
Router Manager Module
|
||||||
|
Handles FastAPI router inclusion and management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from loguru import logger
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class RouterManager:
|
||||||
|
"""Manages FastAPI router inclusion and organization."""
|
||||||
|
|
||||||
|
def __init__(self, app: FastAPI):
|
||||||
|
self.app = app
|
||||||
|
self.included_routers = []
|
||||||
|
self.failed_routers = []
|
||||||
|
|
||||||
|
def include_router_safely(self, router, router_name: str = None) -> bool:
|
||||||
|
"""Include a router safely with error handling."""
|
||||||
|
try:
|
||||||
|
self.app.include_router(router)
|
||||||
|
router_name = router_name or getattr(router, 'prefix', 'unknown')
|
||||||
|
self.included_routers.append(router_name)
|
||||||
|
logger.info(f"✅ Router included successfully: {router_name}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
router_name = router_name or 'unknown'
|
||||||
|
self.failed_routers.append({"name": router_name, "error": str(e)})
|
||||||
|
logger.warning(f"❌ Router inclusion failed: {router_name} - {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def include_core_routers(self) -> bool:
|
||||||
|
"""Include core application routers."""
|
||||||
|
try:
|
||||||
|
logger.info("Including core routers...")
|
||||||
|
|
||||||
|
# Component logic router
|
||||||
|
from api.component_logic import router as component_logic_router
|
||||||
|
self.include_router_safely(component_logic_router, "component_logic")
|
||||||
|
|
||||||
|
# Subscription router
|
||||||
|
from api.subscription_api import router as subscription_router
|
||||||
|
self.include_router_safely(subscription_router, "subscription")
|
||||||
|
|
||||||
|
# GSC router
|
||||||
|
from routers.gsc_auth import router as gsc_auth_router
|
||||||
|
self.include_router_safely(gsc_auth_router, "gsc_auth")
|
||||||
|
|
||||||
|
# WordPress router
|
||||||
|
from routers.wordpress_oauth import router as wordpress_oauth_router
|
||||||
|
self.include_router_safely(wordpress_oauth_router, "wordpress_oauth")
|
||||||
|
|
||||||
|
# SEO tools router
|
||||||
|
from routers.seo_tools import router as seo_tools_router
|
||||||
|
self.include_router_safely(seo_tools_router, "seo_tools")
|
||||||
|
|
||||||
|
# Facebook Writer router
|
||||||
|
from api.facebook_writer.routers import facebook_router
|
||||||
|
self.include_router_safely(facebook_router, "facebook_writer")
|
||||||
|
|
||||||
|
# LinkedIn routers
|
||||||
|
from routers.linkedin import router as linkedin_router
|
||||||
|
self.include_router_safely(linkedin_router, "linkedin")
|
||||||
|
|
||||||
|
from api.linkedin_image_generation import router as linkedin_image_router
|
||||||
|
self.include_router_safely(linkedin_image_router, "linkedin_image")
|
||||||
|
|
||||||
|
# Brainstorm router
|
||||||
|
from api.brainstorm import router as brainstorm_router
|
||||||
|
self.include_router_safely(brainstorm_router, "brainstorm")
|
||||||
|
|
||||||
|
# Hallucination detector and writing assistant
|
||||||
|
from api.hallucination_detector import router as hallucination_detector_router
|
||||||
|
self.include_router_safely(hallucination_detector_router, "hallucination_detector")
|
||||||
|
|
||||||
|
from api.writing_assistant import router as writing_assistant_router
|
||||||
|
self.include_router_safely(writing_assistant_router, "writing_assistant")
|
||||||
|
|
||||||
|
# Content planning and user data
|
||||||
|
from api.content_planning.api.router import router as content_planning_router
|
||||||
|
self.include_router_safely(content_planning_router, "content_planning")
|
||||||
|
|
||||||
|
from api.user_data import router as user_data_router
|
||||||
|
self.include_router_safely(user_data_router, "user_data")
|
||||||
|
|
||||||
|
from api.user_environment import router as user_environment_router
|
||||||
|
self.include_router_safely(user_environment_router, "user_environment")
|
||||||
|
|
||||||
|
# Strategy copilot
|
||||||
|
from api.content_planning.strategy_copilot import router as strategy_copilot_router
|
||||||
|
self.include_router_safely(strategy_copilot_router, "strategy_copilot")
|
||||||
|
|
||||||
|
logger.info("✅ Core routers included successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error including core routers: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def include_optional_routers(self) -> bool:
|
||||||
|
"""Include optional routers with error handling."""
|
||||||
|
try:
|
||||||
|
logger.info("Including optional routers...")
|
||||||
|
|
||||||
|
# AI Blog Writer router
|
||||||
|
try:
|
||||||
|
from api.blog_writer.router import router as blog_writer_router
|
||||||
|
self.include_router_safely(blog_writer_router, "blog_writer")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"AI Blog Writer router not mounted: {e}")
|
||||||
|
|
||||||
|
# Wix Integration router
|
||||||
|
try:
|
||||||
|
from api.wix_routes import router as wix_router
|
||||||
|
self.include_router_safely(wix_router, "wix")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Wix Integration router not mounted: {e}")
|
||||||
|
|
||||||
|
# Blog Writer SEO Analysis router
|
||||||
|
try:
|
||||||
|
from api.blog_writer.seo_analysis import router as blog_seo_analysis_router
|
||||||
|
self.include_router_safely(blog_seo_analysis_router, "blog_seo_analysis")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Blog Writer SEO Analysis router not mounted: {e}")
|
||||||
|
|
||||||
|
# Persona router
|
||||||
|
try:
|
||||||
|
from api.persona_routes import router as persona_router
|
||||||
|
self.include_router_safely(persona_router, "persona")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Persona router not mounted: {e}")
|
||||||
|
|
||||||
|
# Stability AI routers
|
||||||
|
try:
|
||||||
|
from routers.stability import router as stability_router
|
||||||
|
self.include_router_safely(stability_router, "stability")
|
||||||
|
|
||||||
|
from routers.stability_advanced import router as stability_advanced_router
|
||||||
|
self.include_router_safely(stability_advanced_router, "stability_advanced")
|
||||||
|
|
||||||
|
from routers.stability_admin import router as stability_admin_router
|
||||||
|
self.include_router_safely(stability_admin_router, "stability_admin")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Stability AI routers not mounted: {e}")
|
||||||
|
|
||||||
|
# Step 3 Research router
|
||||||
|
try:
|
||||||
|
from api.onboarding_utils.step3_routes import router as step3_research_router
|
||||||
|
self.include_router_safely(step3_research_router, "step3_research")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Step 3 Research router not mounted: {e}")
|
||||||
|
|
||||||
|
logger.info("✅ Optional routers processed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error including optional routers: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_router_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get the status of router inclusion."""
|
||||||
|
return {
|
||||||
|
"included_routers": self.included_routers,
|
||||||
|
"failed_routers": self.failed_routers,
|
||||||
|
"total_included": len(self.included_routers),
|
||||||
|
"total_failed": len(self.failed_routers)
|
||||||
|
}
|
||||||
663
backend/app.py
663
backend/app.py
@@ -3,64 +3,23 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Depends, Request, BackgroundTasks
|
from fastapi import FastAPI, HTTPException, Depends, Request, BackgroundTasks
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from middleware.monitoring_middleware import monitoring_middleware
|
from middleware.monitoring_middleware import monitoring_middleware
|
||||||
|
|
||||||
|
# Import modular utilities
|
||||||
|
from alwrity_utils import HealthChecker, RateLimiter, FrontendServing, RouterManager, OnboardingManager
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Import the new enhanced functions
|
# Import middleware
|
||||||
from api.onboarding import (
|
|
||||||
health_check,
|
|
||||||
initialize_onboarding, # NEW: Batch init endpoint
|
|
||||||
get_onboarding_status,
|
|
||||||
get_onboarding_progress_full,
|
|
||||||
get_step_data,
|
|
||||||
complete_step,
|
|
||||||
skip_step,
|
|
||||||
validate_step_access,
|
|
||||||
get_api_keys,
|
|
||||||
get_api_keys_for_onboarding,
|
|
||||||
save_api_key,
|
|
||||||
validate_api_keys,
|
|
||||||
start_onboarding,
|
|
||||||
complete_onboarding,
|
|
||||||
reset_onboarding,
|
|
||||||
get_resume_info,
|
|
||||||
get_onboarding_config,
|
|
||||||
get_provider_setup_info,
|
|
||||||
get_all_providers_info,
|
|
||||||
validate_provider_key,
|
|
||||||
get_enhanced_validation_status,
|
|
||||||
get_onboarding_summary,
|
|
||||||
get_website_analysis_data,
|
|
||||||
get_research_preferences_data,
|
|
||||||
save_business_info,
|
|
||||||
get_business_info,
|
|
||||||
get_business_info_by_user,
|
|
||||||
update_business_info,
|
|
||||||
# Persona generation endpoints
|
|
||||||
generate_writing_personas,
|
|
||||||
generate_writing_personas_async,
|
|
||||||
get_persona_task_status,
|
|
||||||
assess_persona_quality,
|
|
||||||
regenerate_persona,
|
|
||||||
get_persona_generation_options,
|
|
||||||
# New cache helpers
|
|
||||||
get_latest_persona,
|
|
||||||
save_persona_update,
|
|
||||||
StepCompletionRequest,
|
|
||||||
APIKeyRequest
|
|
||||||
)
|
|
||||||
from middleware.auth_middleware import get_current_user
|
from middleware.auth_middleware import get_current_user
|
||||||
|
|
||||||
# Import component logic endpoints
|
# Import component logic endpoints
|
||||||
@@ -138,554 +97,69 @@ app.add_middleware(
|
|||||||
# Temporarily disabled for Wix testing
|
# Temporarily disabled for Wix testing
|
||||||
# app.middleware("http")(monitoring_middleware)
|
# app.middleware("http")(monitoring_middleware)
|
||||||
|
|
||||||
# Simple rate limiting
|
# Initialize modular utilities
|
||||||
request_counts = defaultdict(list)
|
health_checker = HealthChecker()
|
||||||
RATE_LIMIT_WINDOW = 60 # 60 seconds
|
rate_limiter = RateLimiter(window_seconds=60, max_requests=200)
|
||||||
RATE_LIMIT_MAX_REQUESTS = 200 # Increased for testing - calendar generation polling
|
frontend_serving = FrontendServing(app)
|
||||||
|
router_manager = RouterManager(app)
|
||||||
|
onboarding_manager = OnboardingManager(app)
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def rate_limit_middleware(request: Request, call_next):
|
async def rate_limit_middleware(request: Request, call_next):
|
||||||
"""Simple rate limiting middleware with exemptions for streaming endpoints."""
|
"""Rate limiting middleware using modular utilities."""
|
||||||
try:
|
return await rate_limiter.rate_limit_middleware(request, call_next)
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
|
||||||
current_time = time.time()
|
|
||||||
|
|
||||||
# Exempt streaming endpoints and frequently called endpoints from rate limiting
|
|
||||||
path = request.url.path
|
|
||||||
if any(streaming_path in path for streaming_path in [
|
|
||||||
"/stream/strategies",
|
|
||||||
"/stream/strategic-intelligence",
|
|
||||||
"/stream/keyword-research",
|
|
||||||
"/latest-strategy", # Exempt latest strategy endpoint from rate limiting
|
|
||||||
"/ai-analytics", # Exempt AI analytics endpoint from rate limiting
|
|
||||||
"/gap-analysis", # Exempt gap analysis endpoint from rate limiting
|
|
||||||
"/calendar-events", # Exempt calendar events endpoint from rate limiting
|
|
||||||
"/calendar-generation/progress", # Exempt calendar generation progress from rate limiting
|
|
||||||
"/health" # Exempt health check endpoints from rate limiting
|
|
||||||
]):
|
|
||||||
# Allow streaming endpoints without rate limiting
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Clean old requests
|
|
||||||
request_counts[client_ip] = [req_time for req_time in request_counts[client_ip]
|
|
||||||
if current_time - req_time < RATE_LIMIT_WINDOW]
|
|
||||||
|
|
||||||
# Check rate limit
|
|
||||||
if len(request_counts[client_ip]) >= RATE_LIMIT_MAX_REQUESTS:
|
|
||||||
logger.warning(f"Rate limit exceeded for {client_ip}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=429,
|
|
||||||
content={"detail": "Too many requests", "retry_after": RATE_LIMIT_WINDOW},
|
|
||||||
headers={
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "*",
|
|
||||||
"Access-Control-Allow-Headers": "*"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add current request
|
|
||||||
request_counts[client_ip].append(current_time)
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in rate limiting middleware: {e}")
|
|
||||||
# Continue without rate limiting if there's an error
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Health check endpoint
|
# Health check endpoints using modular utilities
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health():
|
async def health():
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
return health_check()
|
return health_checker.basic_health_check()
|
||||||
|
|
||||||
@app.get("/health/database")
|
@app.get("/health/database")
|
||||||
async def database_health_check():
|
async def database_health():
|
||||||
"""Database health check endpoint including persona tables verification."""
|
"""Database health check endpoint."""
|
||||||
try:
|
return health_checker.database_health_check()
|
||||||
from services.database import get_db_session
|
|
||||||
from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult, PersonaValidationResult
|
|
||||||
|
|
||||||
session = get_db_session()
|
|
||||||
if not session:
|
|
||||||
return {"status": "error", "message": "Could not get database session"}
|
|
||||||
|
|
||||||
# Test all persona tables
|
|
||||||
tables_status = {}
|
|
||||||
try:
|
|
||||||
session.query(WritingPersona).first()
|
|
||||||
tables_status["writing_personas"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
tables_status["writing_personas"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.query(PlatformPersona).first()
|
|
||||||
tables_status["platform_personas"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
tables_status["platform_personas"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.query(PersonaAnalysisResult).first()
|
|
||||||
tables_status["persona_analysis_results"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
tables_status["persona_analysis_results"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.query(PersonaValidationResult).first()
|
|
||||||
tables_status["persona_validation_results"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
tables_status["persona_validation_results"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
# Check if all tables are ok
|
|
||||||
all_ok = all(status == "ok" for status in tables_status.values())
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "healthy" if all_ok else "warning",
|
|
||||||
"message": "Database connection successful" if all_ok else "Some persona tables may have issues",
|
|
||||||
"persona_tables": tables_status,
|
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Database health check failed: {str(e)}",
|
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Onboarding initialization - BATCH ENDPOINT (reduces 4 API calls to 1)
|
@app.get("/health/comprehensive")
|
||||||
@app.get("/api/onboarding/init")
|
async def comprehensive_health():
|
||||||
async def onboarding_init(current_user: dict = Depends(get_current_user)):
|
"""Comprehensive health check endpoint."""
|
||||||
"""
|
return health_checker.comprehensive_health_check()
|
||||||
Batch initialization endpoint - combines user info, status, and progress.
|
|
||||||
This eliminates 3-4 separate API calls on initial load, reducing latency by 60-75%.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await initialize_onboarding(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_init: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Onboarding status endpoints
|
# Rate limiting management endpoints
|
||||||
|
@app.get("/api/rate-limit/status")
|
||||||
|
async def rate_limit_status(request: Request):
|
||||||
|
"""Get current rate limit status for the requesting client."""
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
return rate_limiter.get_rate_limit_status(client_ip)
|
||||||
|
|
||||||
|
@app.post("/api/rate-limit/reset")
|
||||||
|
async def reset_rate_limit(request: Request, client_ip: Optional[str] = None):
|
||||||
|
"""Reset rate limit for a specific client or all clients."""
|
||||||
|
if client_ip is None:
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
return rate_limiter.reset_rate_limit(client_ip)
|
||||||
|
|
||||||
|
# Frontend serving management endpoints
|
||||||
|
@app.get("/api/frontend/status")
|
||||||
|
async def frontend_status():
|
||||||
|
"""Get frontend serving status."""
|
||||||
|
return frontend_serving.get_frontend_status()
|
||||||
|
|
||||||
|
# Router management endpoints
|
||||||
|
@app.get("/api/routers/status")
|
||||||
|
async def router_status():
|
||||||
|
"""Get router inclusion status."""
|
||||||
|
return router_manager.get_router_status()
|
||||||
|
|
||||||
|
# Onboarding management endpoints
|
||||||
@app.get("/api/onboarding/status")
|
@app.get("/api/onboarding/status")
|
||||||
async def onboarding_status(current_user: dict = Depends(get_current_user)):
|
async def onboarding_status():
|
||||||
"""Get the current onboarding status."""
|
"""Get onboarding manager status."""
|
||||||
try:
|
return onboarding_manager.get_onboarding_status()
|
||||||
# Pass current_user explicitly to user-scoped handler
|
|
||||||
return await get_onboarding_status(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
# Preserve HTTP error codes like 401 Unauthorized
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_status: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/progress")
|
# Include routers using modular utilities
|
||||||
async def onboarding_progress(current_user: dict = Depends(get_current_user)):
|
router_manager.include_core_routers()
|
||||||
"""Get the full onboarding progress data."""
|
router_manager.include_optional_routers()
|
||||||
try:
|
|
||||||
return await get_onboarding_progress_full(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_progress: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Step management endpoints
|
|
||||||
@app.get("/api/onboarding/step/{step_number}")
|
|
||||||
async def step_data(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get data for a specific step."""
|
|
||||||
try:
|
|
||||||
return await get_step_data(step_number, current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in step_data: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step/{step_number}/complete")
|
|
||||||
async def step_complete(step_number: int, request: StepCompletionRequest, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Mark a step as completed."""
|
|
||||||
try:
|
|
||||||
return await complete_step(step_number, request, current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in step_complete: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step/{step_number}/skip")
|
|
||||||
async def step_skip(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Skip a step (for optional steps)."""
|
|
||||||
try:
|
|
||||||
return await skip_step(step_number, current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in step_skip: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/step/{step_number}/validate")
|
|
||||||
async def step_validate(step_number: int, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Validate if user can access a specific step."""
|
|
||||||
try:
|
|
||||||
return await validate_step_access(step_number, current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in step_validate: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# API key management endpoints
|
|
||||||
@app.get("/api/onboarding/api-keys")
|
|
||||||
async def api_keys():
|
|
||||||
"""Get all configured API keys (masked)."""
|
|
||||||
try:
|
|
||||||
return await get_api_keys()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in api_keys: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/api-keys/onboarding")
|
|
||||||
async def api_keys_for_onboarding():
|
|
||||||
"""Get all configured API keys for onboarding (unmasked)."""
|
|
||||||
try:
|
|
||||||
return await get_api_keys_for_onboarding()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in api_keys_for_onboarding: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/api-keys")
|
|
||||||
async def api_key_save(request: APIKeyRequest):
|
|
||||||
"""Save an API key for a provider."""
|
|
||||||
try:
|
|
||||||
return await save_api_key(request)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in api_key_save: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/api-keys/validate")
|
|
||||||
async def api_key_validate():
|
|
||||||
"""Validate all configured API keys."""
|
|
||||||
try:
|
|
||||||
return await validate_api_keys()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in api_key_validate: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Onboarding control endpoints
|
|
||||||
@app.post("/api/onboarding/start")
|
|
||||||
async def onboarding_start(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Start a new onboarding session."""
|
|
||||||
try:
|
|
||||||
return await start_onboarding(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_start: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/complete")
|
|
||||||
async def onboarding_complete(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Complete the onboarding process."""
|
|
||||||
try:
|
|
||||||
return await complete_onboarding(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_complete: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/reset")
|
|
||||||
async def onboarding_reset():
|
|
||||||
"""Reset the onboarding progress."""
|
|
||||||
try:
|
|
||||||
return await reset_onboarding()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_reset: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Resume functionality
|
|
||||||
@app.get("/api/onboarding/resume")
|
|
||||||
async def onboarding_resume():
|
|
||||||
"""Get information for resuming onboarding."""
|
|
||||||
try:
|
|
||||||
return await get_resume_info()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_resume: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Configuration endpoints
|
|
||||||
@app.get("/api/onboarding/config")
|
|
||||||
async def onboarding_config():
|
|
||||||
"""Get onboarding configuration and requirements."""
|
|
||||||
try:
|
|
||||||
return get_onboarding_config()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_config: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Enhanced provider endpoints
|
|
||||||
@app.get("/api/onboarding/providers/{provider}/setup")
|
|
||||||
async def provider_setup_info(provider: str):
|
|
||||||
"""Get setup information for a specific provider."""
|
|
||||||
try:
|
|
||||||
return await get_provider_setup_info(provider)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in provider_setup_info: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/providers")
|
|
||||||
async def all_providers_info():
|
|
||||||
"""Get setup information for all providers."""
|
|
||||||
try:
|
|
||||||
return await get_all_providers_info()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in all_providers_info: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/providers/{provider}/validate")
|
|
||||||
async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest):
|
|
||||||
"""Validate a specific provider's API key."""
|
|
||||||
try:
|
|
||||||
return await validate_provider_key(provider, request)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in validate_provider_key: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/validation/enhanced")
|
|
||||||
async def enhanced_validation_status():
|
|
||||||
"""Get enhanced validation status for all configured services."""
|
|
||||||
try:
|
|
||||||
return await get_enhanced_validation_status()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in enhanced_validation_status: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# New endpoints for FinalStep data loading
|
|
||||||
@app.get("/api/onboarding/summary")
|
|
||||||
async def onboarding_summary(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get comprehensive onboarding summary for FinalStep."""
|
|
||||||
try:
|
|
||||||
return await get_onboarding_summary(current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in onboarding_summary: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/website-analysis")
|
|
||||||
async def website_analysis_data(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get website analysis data for FinalStep."""
|
|
||||||
try:
|
|
||||||
return await get_website_analysis_data(current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in website_analysis_data: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/research-preferences")
|
|
||||||
async def research_preferences_data(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get research preferences data for FinalStep."""
|
|
||||||
try:
|
|
||||||
return await get_research_preferences_data(current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in research_preferences_data: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Business Information endpoints
|
|
||||||
@app.post("/api/onboarding/business-info")
|
|
||||||
async def business_info_save(request: 'BusinessInfoRequest'):
|
|
||||||
"""Save business information for users without websites."""
|
|
||||||
try:
|
|
||||||
from models.business_info_request import BusinessInfoRequest
|
|
||||||
return await save_business_info(request)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in business_info_save: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/business-info/{business_info_id}")
|
|
||||||
async def business_info_get(business_info_id: int):
|
|
||||||
"""Get business information by ID."""
|
|
||||||
try:
|
|
||||||
return await get_business_info(business_info_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in business_info_get: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/business-info/user/{user_id}")
|
|
||||||
async def business_info_get_by_user(user_id: int):
|
|
||||||
"""Get business information by user ID."""
|
|
||||||
try:
|
|
||||||
return await get_business_info_by_user(user_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in business_info_get_by_user: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.put("/api/onboarding/business-info/{business_info_id}")
|
|
||||||
async def business_info_update(business_info_id: int, request: 'BusinessInfoRequest'):
|
|
||||||
"""Update business information."""
|
|
||||||
try:
|
|
||||||
from models.business_info_request import BusinessInfoRequest
|
|
||||||
return await update_business_info(business_info_id, request)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in business_info_update: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Persona generation endpoints
|
|
||||||
@app.post("/api/onboarding/step4/generate-personas")
|
|
||||||
async def generate_personas(request: dict, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Generate AI writing personas for Step 4."""
|
|
||||||
try:
|
|
||||||
return await generate_writing_personas(request, current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in generate_personas: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step4/generate-personas-async")
|
|
||||||
async def generate_personas_async(request: dict, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Start async persona generation task."""
|
|
||||||
try:
|
|
||||||
return await generate_writing_personas_async(request, current_user, background_tasks)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in generate_personas_async: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/step4/persona-task/{task_id}")
|
|
||||||
async def get_persona_task(task_id: str):
|
|
||||||
"""Get persona generation task status."""
|
|
||||||
try:
|
|
||||||
return await get_persona_task_status(task_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in get_persona_task: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/step4/persona-latest")
|
|
||||||
async def persona_latest(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get latest cached persona for current user."""
|
|
||||||
try:
|
|
||||||
return await get_latest_persona(current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
# Re-raise HTTP exceptions (like 404) as-is
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in persona_latest: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step4/persona-save")
|
|
||||||
async def persona_save(request: dict, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Save edited persona back to cache."""
|
|
||||||
try:
|
|
||||||
return await save_persona_update(request, current_user)
|
|
||||||
except HTTPException as he:
|
|
||||||
# Re-raise HTTP exceptions as-is
|
|
||||||
raise he
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in persona_save: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step4/assess-persona-quality")
|
|
||||||
async def assess_persona_quality_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Assess the quality of generated personas."""
|
|
||||||
try:
|
|
||||||
return await assess_persona_quality(request, current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in assess_persona_quality: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/onboarding/step4/regenerate-persona")
|
|
||||||
async def regenerate_persona_endpoint(request: dict, current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Regenerate a specific persona with improvements."""
|
|
||||||
try:
|
|
||||||
return await regenerate_persona(request, current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in regenerate_persona: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.get("/api/onboarding/step4/persona-options")
|
|
||||||
async def get_persona_options(current_user: dict = Depends(get_current_user)):
|
|
||||||
"""Get persona generation options and configurations."""
|
|
||||||
try:
|
|
||||||
return await get_persona_generation_options(current_user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in get_persona_options: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
# Include component logic router
|
|
||||||
app.include_router(component_logic_router)
|
|
||||||
|
|
||||||
# Include subscription and usage tracking router
|
|
||||||
app.include_router(subscription_router)
|
|
||||||
|
|
||||||
# Include GSC router
|
|
||||||
from routers.gsc_auth import router as gsc_auth_router
|
|
||||||
app.include_router(gsc_auth_router)
|
|
||||||
|
|
||||||
# Include WordPress router
|
|
||||||
from routers.wordpress_oauth import router as wordpress_oauth_router
|
|
||||||
app.include_router(wordpress_oauth_router)
|
|
||||||
|
|
||||||
# Include SEO tools router
|
|
||||||
app.include_router(seo_tools_router)
|
|
||||||
# Include Facebook Writer router
|
|
||||||
app.include_router(facebook_router)
|
|
||||||
# Include LinkedIn content generation router
|
|
||||||
app.include_router(linkedin_router)
|
|
||||||
# Include LinkedIn image generation router
|
|
||||||
app.include_router(linkedin_image_router)
|
|
||||||
app.include_router(brainstorm_router)
|
|
||||||
|
|
||||||
# Include hallucination detector router
|
|
||||||
app.include_router(hallucination_detector_router)
|
|
||||||
app.include_router(writing_assistant_router)
|
|
||||||
|
|
||||||
# Include user data router
|
|
||||||
# Include content planning router
|
|
||||||
app.include_router(content_planning_router)
|
|
||||||
app.include_router(user_data_router)
|
|
||||||
app.include_router(strategy_copilot_router)
|
|
||||||
app.include_router(user_environment_router)
|
|
||||||
|
|
||||||
# Include AI Blog Writer router
|
|
||||||
try:
|
|
||||||
from api.blog_writer.router import router as blog_writer_router
|
|
||||||
app.include_router(blog_writer_router)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"AI Blog Writer router not mounted: {e}")
|
|
||||||
|
|
||||||
# Include Wix Integration router
|
|
||||||
try:
|
|
||||||
from api.wix_routes import router as wix_router
|
|
||||||
app.include_router(wix_router)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Wix Integration router not mounted: {e}")
|
|
||||||
|
|
||||||
# Include Blog Writer SEO Analysis router (comprehensive SEO analysis)
|
|
||||||
try:
|
|
||||||
from api.blog_writer.seo_analysis import router as blog_seo_analysis_router
|
|
||||||
app.include_router(blog_seo_analysis_router)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Blog Writer SEO Analysis router not mounted: {e}")
|
|
||||||
|
|
||||||
# Include persona router
|
|
||||||
from api.persona_routes import router as persona_router
|
|
||||||
app.include_router(persona_router)
|
|
||||||
|
|
||||||
# Include Stability AI routers
|
|
||||||
from routers.stability import router as stability_router
|
|
||||||
from routers.stability_advanced import router as stability_advanced_router
|
|
||||||
from routers.stability_admin import router as stability_admin_router
|
|
||||||
app.include_router(stability_router)
|
|
||||||
app.include_router(stability_advanced_router)
|
|
||||||
app.include_router(stability_admin_router)
|
|
||||||
|
|
||||||
# Step 3 Research router
|
|
||||||
from api.onboarding_utils.step3_routes import router as step3_research_router
|
|
||||||
app.include_router(step3_research_router)
|
|
||||||
|
|
||||||
# SEO Dashboard endpoints
|
# SEO Dashboard endpoints
|
||||||
@app.get("/api/seo-dashboard/data")
|
@app.get("/api/seo-dashboard/data")
|
||||||
@@ -744,33 +218,14 @@ async def batch_analyze_urls_endpoint(urls: list[str]):
|
|||||||
"""Analyze multiple URLs in batch."""
|
"""Analyze multiple URLs in batch."""
|
||||||
return await batch_analyze_urls(urls)
|
return await batch_analyze_urls(urls)
|
||||||
|
|
||||||
|
# Setup frontend serving using modular utilities
|
||||||
|
frontend_serving.setup_frontend_serving()
|
||||||
|
|
||||||
# Serve React frontend (for production)
|
# Serve React frontend (for production)
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def serve_frontend():
|
async def serve_frontend():
|
||||||
"""Serve the React frontend."""
|
"""Serve the React frontend."""
|
||||||
# Check if frontend build exists
|
return frontend_serving.serve_frontend()
|
||||||
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build")
|
|
||||||
index_html = os.path.join(frontend_path, "index.html")
|
|
||||||
|
|
||||||
if os.path.exists(index_html):
|
|
||||||
return FileResponse(index_html)
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"message": "Frontend not built. Please run 'npm run build' in the frontend directory.",
|
|
||||||
"api_docs": "/api/docs"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mount static files for React app (only if directory exists)
|
|
||||||
try:
|
|
||||||
frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build")
|
|
||||||
static_path = os.path.join(frontend_build_path, "static")
|
|
||||||
if os.path.exists(static_path):
|
|
||||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
|
||||||
logger.info("Frontend static files mounted successfully")
|
|
||||||
else:
|
|
||||||
logger.info("Frontend build directory not found. Static files not mounted.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.info(f"Could not mount static files: {e}")
|
|
||||||
|
|
||||||
# Startup event
|
# Startup event
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
LOG_BASE_DIR = "/workspace/backend/logs"
|
LOG_BASE_DIR = "logs"
|
||||||
os.makedirs(LOG_BASE_DIR, exist_ok=True)
|
os.makedirs(LOG_BASE_DIR, exist_ok=True)
|
||||||
|
|
||||||
# Ensure subdirectories exist
|
# Ensure subdirectories exist
|
||||||
|
|||||||
44
backend/render.yaml
Normal file
44
backend/render.yaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Render deployment configuration for ALwrity Backend
|
||||||
|
services:
|
||||||
|
- type: web
|
||||||
|
name: alwrity-backend
|
||||||
|
env: python
|
||||||
|
buildCommand: pip install -r requirements.txt
|
||||||
|
startCommand: python start_alwrity_backend.py --production
|
||||||
|
healthCheckPath: /health
|
||||||
|
envVars:
|
||||||
|
- key: HOST
|
||||||
|
value: 0.0.0.0
|
||||||
|
- key: PORT
|
||||||
|
value: 8000
|
||||||
|
- key: RELOAD
|
||||||
|
value: false
|
||||||
|
- key: LOG_LEVEL
|
||||||
|
value: INFO
|
||||||
|
- key: PYTHONPATH
|
||||||
|
value: /opt/render/project/src/backend
|
||||||
|
- key: TMPDIR
|
||||||
|
value: /tmp
|
||||||
|
# Add your environment variables here:
|
||||||
|
# - key: GEMINI_API_KEY
|
||||||
|
# value: your_gemini_key_here
|
||||||
|
# - key: OPENAI_API_KEY
|
||||||
|
# value: your_openai_key_here
|
||||||
|
# - key: ANTHROPIC_API_KEY
|
||||||
|
# value: your_anthropic_key_here
|
||||||
|
# - key: MISTRAL_API_KEY
|
||||||
|
# value: your_mistral_key_here
|
||||||
|
# - key: TAVILY_API_KEY
|
||||||
|
# value: your_tavily_key_here
|
||||||
|
# - key: EXA_API_KEY
|
||||||
|
# value: your_exa_key_here
|
||||||
|
# - key: SERPER_API_KEY
|
||||||
|
# value: your_serper_key_here
|
||||||
|
# - key: CLERK_SECRET_KEY
|
||||||
|
# value: your_clerk_secret_here
|
||||||
|
# - key: GSC_REDIRECT_URI
|
||||||
|
# value: https://your-frontend.vercel.app/gsc/callback
|
||||||
|
# - key: WORDPRESS_REDIRECT_URI
|
||||||
|
# value: https://your-frontend.vercel.app/wp/callback
|
||||||
|
# - key: WIX_REDIRECT_URI
|
||||||
|
# value: https://your-frontend.vercel.app/wix/callback
|
||||||
@@ -33,7 +33,7 @@ from middleware.logging_middleware import log_api_call, save_to_file
|
|||||||
router = APIRouter(prefix="/api/seo", tags=["AI SEO Tools"])
|
router = APIRouter(prefix="/api/seo", tags=["AI SEO Tools"])
|
||||||
|
|
||||||
# Configuration for intelligent logging
|
# Configuration for intelligent logging
|
||||||
LOG_DIR = "/workspace/backend/logs/seo_tools"
|
LOG_DIR = "logs/seo_tools"
|
||||||
os.makedirs(LOG_DIR, exist_ok=True)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
# Request/Response Models
|
# Request/Response Models
|
||||||
|
|||||||
@@ -18,14 +18,32 @@ class UserWorkspaceManager:
|
|||||||
|
|
||||||
def __init__(self, db_session: Session):
|
def __init__(self, db_session: Session):
|
||||||
self.db = db_session
|
self.db = db_session
|
||||||
self.base_workspace_dir = Path("lib/workspace")
|
# Use environment-safe paths for production
|
||||||
self.user_workspaces_dir = self.base_workspace_dir / "users"
|
if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"):
|
||||||
|
# In production, use temp directories or skip file operations
|
||||||
|
self.base_workspace_dir = Path("/tmp/alwrity_workspace")
|
||||||
|
self.user_workspaces_dir = self.base_workspace_dir / "users"
|
||||||
|
else:
|
||||||
|
# In development, use local directories
|
||||||
|
self.base_workspace_dir = Path("lib/workspace")
|
||||||
|
self.user_workspaces_dir = self.base_workspace_dir / "users"
|
||||||
|
|
||||||
def create_user_workspace(self, user_id: str) -> Dict[str, Any]:
|
def create_user_workspace(self, user_id: str) -> Dict[str, Any]:
|
||||||
"""Create a complete user workspace with progressive setup."""
|
"""Create a complete user workspace with progressive setup."""
|
||||||
try:
|
try:
|
||||||
logger.info(f"Creating workspace for user {user_id}")
|
logger.info(f"Creating workspace for user {user_id}")
|
||||||
|
|
||||||
|
# Check if we're in production and skip file operations if needed
|
||||||
|
if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"):
|
||||||
|
logger.info("Production environment detected - skipping file workspace creation")
|
||||||
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"workspace_path": "/tmp/alwrity_workspace/users/user_" + user_id,
|
||||||
|
"config": self._create_user_config(user_id),
|
||||||
|
"created_at": datetime.utcnow().isoformat(),
|
||||||
|
"production_mode": True
|
||||||
|
}
|
||||||
|
|
||||||
# Create user-specific directories
|
# Create user-specific directories
|
||||||
user_dir = self.user_workspaces_dir / f"user_{user_id}"
|
user_dir = self.user_workspaces_dir / f"user_{user_id}"
|
||||||
user_dir.mkdir(parents=True, exist_ok=True)
|
user_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -1,524 +1,25 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
ALwrity Backend Server - Comprehensive Startup Script
|
ALwrity Backend Server - Modular Startup Script
|
||||||
Handles setup, dependency installation, and server startup.
|
Handles setup, dependency installation, and server startup using modular utilities.
|
||||||
Run this from the backend directory to set up and start the FastAPI server.
|
Run this from the backend directory to set up and start the FastAPI server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def install_requirements():
|
# Import modular utilities
|
||||||
"""Install required Python packages."""
|
from alwrity_utils import (
|
||||||
print("📦 Installing required packages...")
|
DependencyManager,
|
||||||
|
EnvironmentSetup,
|
||||||
requirements_file = Path(__file__).parent / "requirements.txt"
|
DatabaseSetup,
|
||||||
|
ProductionOptimizer
|
||||||
try:
|
)
|
||||||
subprocess.check_call([
|
|
||||||
sys.executable, "-m", "pip", "install", "-r", str(requirements_file)
|
|
||||||
])
|
|
||||||
print("[OK] All packages installed successfully!")
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f"[ERROR] Error installing packages: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_env_file():
|
|
||||||
"""Create a .env file with default configuration."""
|
|
||||||
env_file = Path(__file__).parent / ".env"
|
|
||||||
|
|
||||||
if env_file.exists():
|
|
||||||
print("[INFO] .env file already exists")
|
|
||||||
return True
|
|
||||||
|
|
||||||
print("🔧 Creating .env file with default configuration...")
|
|
||||||
|
|
||||||
env_content = """# ALwrity Backend Configuration
|
|
||||||
|
|
||||||
# API Keys (Configure these in the onboarding process)
|
def start_backend(enable_reload=False, production_mode=False):
|
||||||
# OPENAI_API_KEY=your_openai_api_key_here
|
|
||||||
# GEMINI_API_KEY=your_gemini_api_key_here
|
|
||||||
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
|
||||||
# MISTRAL_API_KEY=your_mistral_api_key_here
|
|
||||||
|
|
||||||
# Research API Keys (Optional)
|
|
||||||
# TAVILY_API_KEY=your_tavily_api_key_here
|
|
||||||
# SERPER_API_KEY=your_serper_api_key_here
|
|
||||||
# METAPHOR_API_KEY=your_metaphor_api_key_here
|
|
||||||
# FIRECRAWL_API_KEY=your_firecrawl_api_key_here
|
|
||||||
|
|
||||||
# Server Configuration
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=8000
|
|
||||||
DEBUG=true
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(env_file, 'w') as f:
|
|
||||||
f.write(env_content)
|
|
||||||
print("[OK] .env file created successfully!")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Error creating .env file: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def setup_monitoring_tables():
|
|
||||||
"""Set up API monitoring database tables."""
|
|
||||||
print("📊 Setting up API monitoring tables...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import and run the monitoring table creation
|
|
||||||
sys.path.append(str(Path(__file__).parent))
|
|
||||||
from scripts.create_monitoring_tables import create_monitoring_tables
|
|
||||||
|
|
||||||
if create_monitoring_tables():
|
|
||||||
print("[OK] API monitoring tables created successfully!")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("[WARNING] Warning: Failed to create monitoring tables, continuing anyway...")
|
|
||||||
return True # Don't fail startup for monitoring issues
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] Warning: Could not set up monitoring tables: {e}")
|
|
||||||
print(" Monitoring will be disabled. Continuing startup...")
|
|
||||||
return True # Don't fail startup for monitoring issues
|
|
||||||
|
|
||||||
def setup_billing_tables():
|
|
||||||
"""Set up billing and subscription database tables."""
|
|
||||||
print("💳 Setting up billing and subscription tables...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import and run the billing table creation
|
|
||||||
sys.path.append(str(Path(__file__).parent))
|
|
||||||
from scripts.create_billing_tables import create_billing_tables, check_existing_tables
|
|
||||||
from services.database import DATABASE_URL
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
# Create engine to check existing tables
|
|
||||||
engine = create_engine(DATABASE_URL, echo=False)
|
|
||||||
|
|
||||||
# Check existing tables
|
|
||||||
if not check_existing_tables(engine):
|
|
||||||
print("[OK] Billing tables already exist, skipping creation")
|
|
||||||
return True
|
|
||||||
|
|
||||||
if create_billing_tables():
|
|
||||||
print("[OK] Billing and subscription tables created successfully!")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("[WARNING] Warning: Failed to create billing tables, continuing anyway...")
|
|
||||||
return True # Don't fail startup for billing issues
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] Warning: Could not set up billing tables: {e}")
|
|
||||||
print(" Billing system will be disabled. Continuing startup...")
|
|
||||||
return True # Don't fail startup for billing issues
|
|
||||||
|
|
||||||
def setup_monitoring_middleware():
|
|
||||||
"""Set up monitoring middleware in app.py if not already present."""
|
|
||||||
print("🔍 Setting up API monitoring middleware...")
|
|
||||||
|
|
||||||
app_file = Path(__file__).parent / "app.py"
|
|
||||||
|
|
||||||
if not app_file.exists():
|
|
||||||
print("[WARNING] Warning: app.py not found, skipping middleware setup")
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(app_file, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Check if monitoring middleware is already set up
|
|
||||||
if "monitoring_middleware" in content:
|
|
||||||
print("[OK] Monitoring middleware already configured")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Add monitoring middleware import and setup
|
|
||||||
monitoring_import = "from middleware.monitoring_middleware import monitoring_middleware\n"
|
|
||||||
monitoring_setup = "app.middleware(\"http\")(monitoring_middleware)\n"
|
|
||||||
|
|
||||||
# Find the right place to add the import (after other imports)
|
|
||||||
lines = content.split('\n')
|
|
||||||
import_end_index = 0
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith('import ') or line.strip().startswith('from '):
|
|
||||||
import_end_index = i + 1
|
|
||||||
elif line.strip() and not line.strip().startswith('#'):
|
|
||||||
break
|
|
||||||
|
|
||||||
# Insert monitoring import
|
|
||||||
lines.insert(import_end_index, monitoring_import)
|
|
||||||
|
|
||||||
# Find the right place to add middleware setup (after app creation)
|
|
||||||
app_creation_index = -1
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if 'app = FastAPI(' in line or 'app = FastAPI()' in line:
|
|
||||||
app_creation_index = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if app_creation_index != -1:
|
|
||||||
# Find the end of app configuration
|
|
||||||
setup_index = app_creation_index + 1
|
|
||||||
for i in range(app_creation_index + 1, len(lines)):
|
|
||||||
if lines[i].strip() and not lines[i].strip().startswith('#'):
|
|
||||||
setup_index = i + 1
|
|
||||||
break
|
|
||||||
|
|
||||||
lines.insert(setup_index, monitoring_setup)
|
|
||||||
|
|
||||||
# Write back to file
|
|
||||||
with open(app_file, 'w') as f:
|
|
||||||
f.write('\n'.join(lines))
|
|
||||||
|
|
||||||
print("[OK] Monitoring middleware configured successfully!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] Warning: Could not set up monitoring middleware: {e}")
|
|
||||||
print(" Monitoring will be disabled. Continuing startup...")
|
|
||||||
return True # Don't fail startup for monitoring issues
|
|
||||||
|
|
||||||
def setup_spacy_model():
|
|
||||||
"""Set up spaCy English model for linguistic analysis."""
|
|
||||||
print("Setting up spaCy English model...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
import spacy
|
|
||||||
|
|
||||||
# Check if en_core_web_sm model is already installed
|
|
||||||
model_name = "en_core_web_sm"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to load the model directly
|
|
||||||
nlp = spacy.load(model_name)
|
|
||||||
|
|
||||||
# Test the model with a simple sentence
|
|
||||||
test_doc = nlp("This is a test sentence.")
|
|
||||||
if test_doc and len(test_doc) > 0:
|
|
||||||
print(f"SUCCESS: spaCy model '{model_name}' is already installed and working")
|
|
||||||
print(f" Test: Processed {len(test_doc)} tokens successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise OSError("Model loaded but not functioning correctly")
|
|
||||||
|
|
||||||
except OSError:
|
|
||||||
print(f"INFO: spaCy model '{model_name}' not found or not working, downloading...")
|
|
||||||
|
|
||||||
# Try to download the model using subprocess
|
|
||||||
try:
|
|
||||||
print(f" Downloading {model_name}...")
|
|
||||||
result = subprocess.run([
|
|
||||||
sys.executable, "-m", "spacy", "download", model_name
|
|
||||||
], capture_output=True, text=True, timeout=300) # 5 minute timeout
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f" SUCCESS: Model download completed")
|
|
||||||
else:
|
|
||||||
print(f" WARNING: Download warning: {result.stderr}")
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
print(f" ERROR: Download timed out after 5 minutes")
|
|
||||||
return False
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f" ERROR: Download failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Verify the model was downloaded correctly
|
|
||||||
try:
|
|
||||||
nlp = spacy.load(model_name)
|
|
||||||
|
|
||||||
# Test the model
|
|
||||||
test_doc = nlp("This is a test sentence.")
|
|
||||||
if test_doc and len(test_doc) > 0:
|
|
||||||
print(f"SUCCESS: spaCy model '{model_name}' downloaded and verified successfully")
|
|
||||||
print(f" Test: Processed {len(test_doc)} tokens successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"ERROR: Model downloaded but not functioning correctly")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except OSError as e:
|
|
||||||
print(f"ERROR: Model downloaded but failed to load: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f"ERROR: Error downloading spaCy model: {e}")
|
|
||||||
print(" Manual installation required:")
|
|
||||||
print(" 1. Install spaCy: pip install spacy>=3.7.0")
|
|
||||||
print(" 2. Download model: python -m spacy download en_core_web_sm")
|
|
||||||
print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"")
|
|
||||||
print(" 4. Restart the backend")
|
|
||||||
return False
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"ERROR: spaCy not installed: {e}")
|
|
||||||
print(" Manual installation required:")
|
|
||||||
print(" 1. Install spaCy: pip install spacy>=3.7.0")
|
|
||||||
print(" 2. Download model: python -m spacy download en_core_web_sm")
|
|
||||||
print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"")
|
|
||||||
print(" 4. Restart the backend")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Error setting up spaCy model: {e}")
|
|
||||||
print(" Manual installation required:")
|
|
||||||
print(" 1. Install spaCy: pip install spacy>=3.7.0")
|
|
||||||
print(" 2. Download model: python -m spacy download en_core_web_sm")
|
|
||||||
print(" 3. Test setup: python -c \"import spacy; nlp=spacy.load('en_core_web_sm'); print('spaCy working!')\"")
|
|
||||||
print(" 4. Restart the backend")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def setup_nltk_data():
|
|
||||||
"""Set up required NLTK data for linguistic analysis."""
|
|
||||||
print("Setting up NLTK data...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
import nltk
|
|
||||||
|
|
||||||
# Required NLTK data packages
|
|
||||||
required_data = [
|
|
||||||
'punkt_tab', # Updated for newer NLTK versions
|
|
||||||
'stopwords',
|
|
||||||
'averaged_perceptron_tagger_eng', # Updated for newer NLTK versions
|
|
||||||
'wordnet',
|
|
||||||
'omw-1.4'
|
|
||||||
]
|
|
||||||
|
|
||||||
for data_package in required_data:
|
|
||||||
try:
|
|
||||||
nltk.data.find(f'tokenizers/{data_package}' if data_package in ['punkt', 'punkt_tab']
|
|
||||||
else f'corpora/{data_package}' if data_package in ['stopwords', 'wordnet', 'omw-1.4']
|
|
||||||
else f'taggers/{data_package}' if data_package in ['averaged_perceptron_tagger', 'averaged_perceptron_tagger_eng']
|
|
||||||
else f'corpora/{data_package}')
|
|
||||||
print(f" SUCCESS: {data_package}")
|
|
||||||
except LookupError:
|
|
||||||
print(f" INFO: Downloading {data_package}...")
|
|
||||||
nltk.download(data_package, quiet=True)
|
|
||||||
print(f" SUCCESS: {data_package} downloaded")
|
|
||||||
|
|
||||||
print("SUCCESS: All required NLTK data is available")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Error setting up NLTK data: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_dependencies():
|
|
||||||
"""Check if required dependencies are installed."""
|
|
||||||
print("🔍 Checking dependencies...")
|
|
||||||
|
|
||||||
required_packages = [
|
|
||||||
'fastapi',
|
|
||||||
'uvicorn',
|
|
||||||
'pydantic',
|
|
||||||
'loguru',
|
|
||||||
'openai',
|
|
||||||
'google.generativeai',
|
|
||||||
'anthropic',
|
|
||||||
'mistralai',
|
|
||||||
'sqlalchemy',
|
|
||||||
'spacy', # Added spaCy to required packages
|
|
||||||
'nltk' # Added NLTK to required packages
|
|
||||||
]
|
|
||||||
|
|
||||||
missing_packages = []
|
|
||||||
|
|
||||||
for package in required_packages:
|
|
||||||
try:
|
|
||||||
__import__(package.replace('-', '_'))
|
|
||||||
print(f" [OK] {package}")
|
|
||||||
except ImportError:
|
|
||||||
print(f" [ERROR] {package} - MISSING")
|
|
||||||
missing_packages.append(package)
|
|
||||||
|
|
||||||
if missing_packages:
|
|
||||||
print(f"\n[ERROR] Missing packages: {', '.join(missing_packages)}")
|
|
||||||
print("Installing missing packages...")
|
|
||||||
return install_requirements()
|
|
||||||
else:
|
|
||||||
print("\n[OK] All dependencies are available!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def setup_environment():
|
|
||||||
"""Set up the environment for the backend."""
|
|
||||||
print("🔧 Setting up environment...")
|
|
||||||
|
|
||||||
# Create necessary directories
|
|
||||||
directories = [
|
|
||||||
"lib/workspace/alwrity_content",
|
|
||||||
"lib/workspace/alwrity_web_research",
|
|
||||||
"lib/workspace/alwrity_prompts",
|
|
||||||
"lib/workspace/alwrity_config"
|
|
||||||
]
|
|
||||||
|
|
||||||
for directory in directories:
|
|
||||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
|
||||||
print(f" [OK] Created directory: {directory}")
|
|
||||||
|
|
||||||
# Create .env file if it doesn't exist
|
|
||||||
create_env_file()
|
|
||||||
|
|
||||||
# Set up monitoring
|
|
||||||
setup_monitoring_tables()
|
|
||||||
setup_monitoring_middleware()
|
|
||||||
|
|
||||||
# Set up billing and subscription system
|
|
||||||
setup_billing_tables()
|
|
||||||
|
|
||||||
# Set up persona tables
|
|
||||||
if setup_persona_tables():
|
|
||||||
# Verify persona tables were created successfully
|
|
||||||
verify_persona_tables()
|
|
||||||
else:
|
|
||||||
print("[WARNING] Warning: Persona tables setup failed, but continuing...")
|
|
||||||
|
|
||||||
# Set up linguistic analysis dependencies (Required for persona generation)
|
|
||||||
print("🧠 Setting up linguistic analysis dependencies...")
|
|
||||||
|
|
||||||
# Set up spaCy model (REQUIRED for persona generation)
|
|
||||||
if not setup_spacy_model():
|
|
||||||
print("[ERROR] CRITICAL: spaCy model setup failed - persona generation will not work!")
|
|
||||||
print(" Please ensure spaCy is installed and en_core_web_sm model is available")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Set up NLTK data (supplementary to spaCy)
|
|
||||||
if not setup_nltk_data():
|
|
||||||
print("[WARNING] Warning: NLTK data setup failed, but continuing...")
|
|
||||||
|
|
||||||
print("[OK] Environment setup complete")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def setup_persona_tables():
|
|
||||||
"""Set up persona database tables."""
|
|
||||||
print("🔧 Setting up persona tables...")
|
|
||||||
try:
|
|
||||||
from services.database import engine
|
|
||||||
from models.persona_models import Base as PersonaBase
|
|
||||||
|
|
||||||
# Create persona tables
|
|
||||||
PersonaBase.metadata.create_all(bind=engine)
|
|
||||||
print("[OK] Persona tables created successfully")
|
|
||||||
|
|
||||||
# Verify tables were created
|
|
||||||
from sqlalchemy import inspect
|
|
||||||
inspector = inspect(engine)
|
|
||||||
tables = inspector.get_table_names()
|
|
||||||
|
|
||||||
persona_tables = [
|
|
||||||
'writing_personas',
|
|
||||||
'platform_personas',
|
|
||||||
'persona_analysis_results',
|
|
||||||
'persona_validation_results'
|
|
||||||
]
|
|
||||||
|
|
||||||
created_tables = [table for table in persona_tables if table in tables]
|
|
||||||
print(f"[OK] Verified persona tables created: {created_tables}")
|
|
||||||
|
|
||||||
if len(created_tables) != len(persona_tables):
|
|
||||||
missing = [table for table in persona_tables if table not in created_tables]
|
|
||||||
print(f"[WARNING] Warning: Missing persona tables: {missing}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Error setting up persona tables: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def verify_persona_tables():
|
|
||||||
"""Verify that persona tables exist and are accessible."""
|
|
||||||
print("🔍 Verifying persona tables...")
|
|
||||||
try:
|
|
||||||
from services.database import get_db_session
|
|
||||||
from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult, PersonaValidationResult
|
|
||||||
|
|
||||||
session = get_db_session()
|
|
||||||
if session:
|
|
||||||
# Try to query all persona tables to verify they exist
|
|
||||||
session.query(WritingPersona).first()
|
|
||||||
session.query(PlatformPersona).first()
|
|
||||||
session.query(PersonaAnalysisResult).first()
|
|
||||||
session.query(PersonaValidationResult).first()
|
|
||||||
session.close()
|
|
||||||
print("[OK] All persona tables verified successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("[WARNING] Warning: Could not get database session")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] Warning: Could not verify persona tables: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def verify_linguistic_analyzer():
|
|
||||||
"""Verify that the linguistic analyzer is working correctly."""
|
|
||||||
print("Verifying linguistic analyzer setup...")
|
|
||||||
try:
|
|
||||||
from services.persona.enhanced_linguistic_analyzer import EnhancedLinguisticAnalyzer
|
|
||||||
|
|
||||||
# Try to initialize the linguistic analyzer
|
|
||||||
analyzer = EnhancedLinguisticAnalyzer()
|
|
||||||
|
|
||||||
# Test with a sample text
|
|
||||||
test_texts = [
|
|
||||||
"This is a test sentence for linguistic analysis.",
|
|
||||||
"ALwrity provides high-quality AI writing assistance.",
|
|
||||||
"The persona generation system uses advanced NLP techniques."
|
|
||||||
]
|
|
||||||
|
|
||||||
# Perform a simple analysis
|
|
||||||
analysis_result = analyzer.analyze_writing_style(test_texts)
|
|
||||||
|
|
||||||
if analysis_result and 'basic_metrics' in analysis_result:
|
|
||||||
print("SUCCESS: Linguistic analyzer verified successfully")
|
|
||||||
print(f" Analyzed {len(test_texts)} text samples")
|
|
||||||
print(f" Analysis keys: {list(analysis_result.keys())}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("WARNING: Linguistic analyzer returned unexpected result")
|
|
||||||
print(f" Result: {analysis_result}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"WARNING: Could not verify linguistic analyzer: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def verify_billing_tables():
|
|
||||||
"""Verify that billing and subscription tables exist and are accessible."""
|
|
||||||
print("🔍 Verifying billing and subscription tables...")
|
|
||||||
try:
|
|
||||||
from services.database import get_db_session
|
|
||||||
from models.subscription_models import (
|
|
||||||
SubscriptionPlan, UserSubscription, APIUsageLog,
|
|
||||||
UsageSummary, APIProviderPricing, UsageAlert
|
|
||||||
)
|
|
||||||
|
|
||||||
session = get_db_session()
|
|
||||||
if session:
|
|
||||||
# Try to query all billing tables to verify they exist
|
|
||||||
session.query(SubscriptionPlan).first()
|
|
||||||
session.query(UserSubscription).first()
|
|
||||||
session.query(APIUsageLog).first()
|
|
||||||
session.query(UsageSummary).first()
|
|
||||||
session.query(APIProviderPricing).first()
|
|
||||||
session.query(UsageAlert).first()
|
|
||||||
session.close()
|
|
||||||
print("[OK] All billing and subscription tables verified successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("[WARNING] Warning: Could not get database session")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] Warning: Could not verify billing tables: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start_backend(enable_reload=False):
|
|
||||||
"""Start the backend server."""
|
"""Start the backend server."""
|
||||||
print("🚀 Starting ALwrity Backend...")
|
print("🚀 Starting ALwrity Backend...")
|
||||||
|
|
||||||
@@ -527,7 +28,7 @@ def start_backend(enable_reload=False):
|
|||||||
os.environ.setdefault("PORT", "8000")
|
os.environ.setdefault("PORT", "8000")
|
||||||
|
|
||||||
# Set reload based on argument or environment variable
|
# Set reload based on argument or environment variable
|
||||||
if enable_reload:
|
if enable_reload and not production_mode:
|
||||||
os.environ.setdefault("RELOAD", "true")
|
os.environ.setdefault("RELOAD", "true")
|
||||||
print(" 🔄 Development mode: Auto-reload enabled")
|
print(" 🔄 Development mode: Auto-reload enabled")
|
||||||
else:
|
else:
|
||||||
@@ -553,26 +54,20 @@ def start_backend(enable_reload=False):
|
|||||||
init_database()
|
init_database()
|
||||||
print("[OK] Database initialized successfully")
|
print("[OK] Database initialized successfully")
|
||||||
|
|
||||||
# Verify persona tables exist
|
|
||||||
verify_persona_tables()
|
|
||||||
|
|
||||||
# Verify linguistic analyzer is working
|
|
||||||
verify_linguistic_analyzer()
|
|
||||||
|
|
||||||
# Verify billing tables exist
|
|
||||||
verify_billing_tables()
|
|
||||||
|
|
||||||
print("\n🌐 Backend is starting...")
|
print("\n🌐 Backend is starting...")
|
||||||
print(" 📖 API Documentation: http://localhost:8000/api/docs")
|
print(" 📖 API Documentation: http://localhost:8000/api/docs")
|
||||||
print(" 🔍 Health Check: http://localhost:8000/health")
|
print(" 🔍 Health Check: http://localhost:8000/health")
|
||||||
print(" 📊 ReDoc: http://localhost:8000/api/redoc")
|
print(" 📊 ReDoc: http://localhost:8000/api/redoc")
|
||||||
print(" 📈 API Monitoring: http://localhost:8000/api/content-planning/monitoring/health")
|
|
||||||
print(" 💳 Billing Dashboard: http://localhost:8000/api/subscription/plans")
|
if not production_mode:
|
||||||
print(" 📊 Usage Tracking: http://localhost:8000/api/subscription/usage/demo")
|
print(" 📈 API Monitoring: http://localhost:8000/api/content-planning/monitoring/health")
|
||||||
|
print(" 💳 Billing Dashboard: http://localhost:8000/api/subscription/plans")
|
||||||
|
print(" 📊 Usage Tracking: http://localhost:8000/api/subscription/usage/demo")
|
||||||
|
|
||||||
print("\n[STOP] Press Ctrl+C to stop the server")
|
print("\n[STOP] Press Ctrl+C to stop the server")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("\n💡 Usage:")
|
print("\n💡 Usage:")
|
||||||
print(" Production mode (default): python start_alwrity_backend.py")
|
print(" Production mode: python start_alwrity_backend.py --production")
|
||||||
print(" Development mode: python start_alwrity_backend.py --dev")
|
print(" Development mode: python start_alwrity_backend.py --dev")
|
||||||
print(" With auto-reload: python start_alwrity_backend.py --reload")
|
print(" With auto-reload: python start_alwrity_backend.py --reload")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@@ -606,7 +101,8 @@ def start_backend(enable_reload=False):
|
|||||||
"temp/*",
|
"temp/*",
|
||||||
"middleware/*",
|
"middleware/*",
|
||||||
"models/*",
|
"models/*",
|
||||||
"scripts/*"
|
"scripts/*",
|
||||||
|
"alwrity_utils/*"
|
||||||
],
|
],
|
||||||
reload_includes=[
|
reload_includes=[
|
||||||
"app.py",
|
"app.py",
|
||||||
@@ -624,16 +120,25 @@ def start_backend(enable_reload=False):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to set up and start the backend."""
|
"""Main function to set up and start the backend."""
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
parser = argparse.ArgumentParser(description="ALwrity Backend Server")
|
parser = argparse.ArgumentParser(description="ALwrity Backend Server")
|
||||||
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
||||||
parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)")
|
parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)")
|
||||||
|
parser.add_argument("--production", action="store_true", help="Enable production mode (optimized for deployment)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Determine mode
|
||||||
|
production_mode = args.production
|
||||||
|
enable_reload = (args.reload or args.dev) and not production_mode
|
||||||
|
|
||||||
print("ALwrity Backend Server")
|
print("ALwrity Backend Server")
|
||||||
print("=" * 40)
|
print("=" * 40)
|
||||||
|
print(f"Mode: {'PRODUCTION' if production_mode else 'DEVELOPMENT'}")
|
||||||
|
print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
# Check if we're in the right directory
|
# Check if we're in the right directory
|
||||||
if not os.path.exists("app.py"):
|
if not os.path.exists("app.py"):
|
||||||
@@ -642,21 +147,63 @@ def main():
|
|||||||
print(" Expected files:", [f for f in os.listdir('.') if f.endswith('.py')])
|
print(" Expected files:", [f for f in os.listdir('.') if f.endswith('.py')])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Initialize modular components
|
||||||
|
dependency_manager = DependencyManager()
|
||||||
|
environment_setup = EnvironmentSetup(production_mode=production_mode)
|
||||||
|
database_setup = DatabaseSetup(production_mode=production_mode)
|
||||||
|
production_optimizer = ProductionOptimizer()
|
||||||
|
|
||||||
|
# Apply production optimizations if needed
|
||||||
|
if production_mode:
|
||||||
|
if not production_optimizer.apply_production_optimizations():
|
||||||
|
print("[ERROR] Production optimization failed")
|
||||||
|
return False
|
||||||
|
|
||||||
# Check and install dependencies
|
# Check and install dependencies
|
||||||
if not check_dependencies():
|
critical_ok, missing_critical = dependency_manager.check_critical_dependencies()
|
||||||
print("[ERROR] Failed to install dependencies")
|
if not critical_ok:
|
||||||
return False
|
print("[ERROR] Critical dependencies missing, installing...")
|
||||||
|
if not dependency_manager.install_requirements():
|
||||||
|
print("[ERROR] Failed to install dependencies")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check optional dependencies (non-critical)
|
||||||
|
dependency_manager.check_optional_dependencies()
|
||||||
|
|
||||||
# Setup environment
|
# Setup environment
|
||||||
if not setup_environment():
|
if not environment_setup.setup_directories():
|
||||||
print("[ERROR] Environment setup failed")
|
print("[ERROR] Directory setup failed")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Start backend with reload option
|
if not environment_setup.setup_environment_variables():
|
||||||
enable_reload = args.reload or args.dev
|
print("[ERROR] Environment variable setup failed")
|
||||||
return start_backend(enable_reload=enable_reload)
|
return False
|
||||||
|
|
||||||
|
# Create .env file only in development
|
||||||
|
if not production_mode:
|
||||||
|
environment_setup.create_env_file()
|
||||||
|
|
||||||
|
# Setup database
|
||||||
|
if not database_setup.setup_essential_tables():
|
||||||
|
print("[WARNING] Database setup had issues, continuing...")
|
||||||
|
|
||||||
|
# Setup advanced features only in development
|
||||||
|
if not production_mode:
|
||||||
|
database_setup.setup_advanced_tables()
|
||||||
|
database_setup.verify_tables()
|
||||||
|
|
||||||
|
# Setup linguistic analysis (skip in production)
|
||||||
|
if not production_mode and not production_optimizer.skip_linguistic_setup():
|
||||||
|
if not production_optimizer.skip_spacy_setup():
|
||||||
|
dependency_manager.setup_spacy_model()
|
||||||
|
if not production_optimizer.skip_nltk_setup():
|
||||||
|
dependency_manager.setup_nltk_data()
|
||||||
|
|
||||||
|
# Start backend
|
||||||
|
return start_backend(enable_reload=enable_reload, production_mode=production_mode)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = main()
|
success = main()
|
||||||
if not success:
|
if not success:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
11
frontend/env.production.example
Normal file
11
frontend/env.production.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Production Environment Variables for Vercel
|
||||||
|
# Copy this file to .env.production and update with your actual values
|
||||||
|
|
||||||
|
# Backend API URL (from Railway/Render deployment)
|
||||||
|
REACT_APP_API_URL=https://your-backend-url.railway.app
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
REACT_APP_ENVIRONMENT=production
|
||||||
|
|
||||||
|
# Optional: Custom domain for OAuth redirects
|
||||||
|
# REACT_APP_DOMAIN=your-custom-domain.com
|
||||||
@@ -7,9 +7,17 @@ export const setAuthTokenGetter = (getter: () => Promise<string | null>) => {
|
|||||||
authTokenGetter = getter;
|
authTokenGetter = getter;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a shared axios instance for all API calls (same-origin; CRA proxy forwards to backend)
|
// Get API URL from environment variables
|
||||||
|
const getApiUrl = () => {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
return process.env.REACT_APP_API_URL || 'https://your-backend-url.railway.app';
|
||||||
|
}
|
||||||
|
return ''; // Use proxy in development
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a shared axios instance for all API calls
|
||||||
export const apiClient = axios.create({
|
export const apiClient = axios.create({
|
||||||
baseURL: '',
|
baseURL: getApiUrl(),
|
||||||
timeout: 60000, // Increased to 60 seconds for regular API calls
|
timeout: 60000, // Increased to 60 seconds for regular API calls
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -18,7 +26,7 @@ export const apiClient = axios.create({
|
|||||||
|
|
||||||
// Create a specialized client for AI operations with extended timeout
|
// Create a specialized client for AI operations with extended timeout
|
||||||
export const aiApiClient = axios.create({
|
export const aiApiClient = axios.create({
|
||||||
baseURL: '',
|
baseURL: getApiUrl(),
|
||||||
timeout: 180000, // 3 minutes timeout for AI operations (matching 20-25 second responses)
|
timeout: 180000, // 3 minutes timeout for AI operations (matching 20-25 second responses)
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -27,7 +35,7 @@ export const aiApiClient = axios.create({
|
|||||||
|
|
||||||
// Create a specialized client for long-running operations like SEO analysis
|
// Create a specialized client for long-running operations like SEO analysis
|
||||||
export const longRunningApiClient = axios.create({
|
export const longRunningApiClient = axios.create({
|
||||||
baseURL: '',
|
baseURL: getApiUrl(),
|
||||||
timeout: 300000, // 5 minutes timeout for SEO analysis
|
timeout: 300000, // 5 minutes timeout for SEO analysis
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -36,7 +44,7 @@ export const longRunningApiClient = axios.create({
|
|||||||
|
|
||||||
// Create a specialized client for polling operations with reasonable timeout
|
// Create a specialized client for polling operations with reasonable timeout
|
||||||
export const pollingApiClient = axios.create({
|
export const pollingApiClient = axios.create({
|
||||||
baseURL: '',
|
baseURL: getApiUrl(),
|
||||||
timeout: 60000, // 60 seconds timeout for polling status checks
|
timeout: 60000, // 60 seconds timeout for polling status checks
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -42,53 +42,18 @@ export const useWixConnection = () => {
|
|||||||
if (connectedFlag && tokensRaw) {
|
if (connectedFlag && tokensRaw) {
|
||||||
const tokens = JSON.parse(tokensRaw);
|
const tokens = JSON.parse(tokensRaw);
|
||||||
|
|
||||||
// Try to get actual site information from Wix API
|
// Set connected status with site information from tokens
|
||||||
try {
|
setStatus({
|
||||||
const { createClient, OAuthStrategy } = await import('@wix/sdk');
|
connected: true,
|
||||||
const wixClient = createClient({
|
sites: [{
|
||||||
auth: OAuthStrategy({ clientId: '75d88e36-1c76-4009-b769-15f4654556df' })
|
id: 'wix-site-1',
|
||||||
});
|
blog_url: 'Connected Wix Site',
|
||||||
wixClient.auth.setTokens(tokens);
|
blog_id: 'wix-blog',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
// Get member info to extract site URL
|
scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE'
|
||||||
const memberInfo = await wixClient.auth.getMemberInfo();
|
}],
|
||||||
console.log('Wix member info:', memberInfo);
|
total_sites: 1
|
||||||
|
});
|
||||||
// Try to extract site URL from member info or use a default
|
|
||||||
let siteUrl = 'Connected Wix Site';
|
|
||||||
if (memberInfo?.member?.email) {
|
|
||||||
// Extract domain from email or use email as identifier
|
|
||||||
const email = memberInfo.member.email;
|
|
||||||
const domain = email.split('@')[1];
|
|
||||||
siteUrl = `https://${domain}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus({
|
|
||||||
connected: true,
|
|
||||||
sites: [{
|
|
||||||
id: 'wix-site-1',
|
|
||||||
blog_url: siteUrl,
|
|
||||||
blog_id: 'wix-blog',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE'
|
|
||||||
}],
|
|
||||||
total_sites: 1
|
|
||||||
});
|
|
||||||
} catch (apiError) {
|
|
||||||
console.log('Wix API error, using fallback:', apiError);
|
|
||||||
// Fallback if API call fails
|
|
||||||
setStatus({
|
|
||||||
connected: true,
|
|
||||||
sites: [{
|
|
||||||
id: 'wix-site-1',
|
|
||||||
blog_url: 'Connected Wix Site',
|
|
||||||
blog_id: 'wix-blog',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE'
|
|
||||||
}],
|
|
||||||
total_sites: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Wix status checked: connected via sessionStorage');
|
console.log('Wix status checked: connected via sessionStorage');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
21
vercel.json
Normal file
21
vercel.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "frontend/package.json",
|
||||||
|
"use": "@vercel/static-build",
|
||||||
|
"config": {
|
||||||
|
"distDir": "build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "/frontend/$1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"REACT_APP_ENVIRONMENT": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user