ALwrity onboarding final step
This commit is contained in:
489
docs/EXAMPLES_USER_API_KEYS.md
Normal file
489
docs/EXAMPLES_USER_API_KEYS.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# User API Key Context - Usage Examples
|
||||
|
||||
This document shows how to use the `UserAPIKeyContext` in your backend services to ensure user-specific API keys are used.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### **1. Basic Usage in FastAPI Endpoint**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.user_api_key_context import user_api_keys
|
||||
import google.generativeai as genai
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/api/generate-content")
|
||||
async def generate_content(
|
||||
prompt: str,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
user_id = current_user.get('user_id')
|
||||
|
||||
# Get user-specific API keys
|
||||
with user_api_keys(user_id) as keys:
|
||||
gemini_key = keys.get('gemini')
|
||||
|
||||
if not gemini_key:
|
||||
raise HTTPException(status_code=400, detail="Gemini API key not configured")
|
||||
|
||||
# Configure Gemini with user's key
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
# Generate content using this user's quota
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return {
|
||||
"content": response.text,
|
||||
"user_id": user_id # For debugging
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples by Use Case
|
||||
|
||||
### **Example 1: Blog Writer Service**
|
||||
|
||||
**File: `backend/services/blog_writer_service.py`**
|
||||
|
||||
```python
|
||||
from services.user_api_key_context import user_api_keys, get_gemini_key
|
||||
import google.generativeai as genai
|
||||
|
||||
class BlogWriterService:
|
||||
"""
|
||||
Service for generating blog content using user-specific API keys.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
|
||||
async def generate_blog_outline(self, topic: str) -> dict:
|
||||
"""Generate blog outline using user's Gemini API key."""
|
||||
|
||||
# Method 1: Using context manager (recommended)
|
||||
with user_api_keys(self.user_id) as keys:
|
||||
gemini_key = keys.get('gemini')
|
||||
|
||||
if not gemini_key:
|
||||
raise ValueError(f"No Gemini API key found for user {self.user_id}")
|
||||
|
||||
# Configure Gemini with user's key
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
prompt = f"Create a detailed blog outline for: {topic}"
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return {
|
||||
"outline": response.text,
|
||||
"topic": topic,
|
||||
"user_id": self.user_id
|
||||
}
|
||||
|
||||
async def generate_blog_section(self, section_heading: str, context: str) -> str:
|
||||
"""Generate blog section using user's Gemini API key."""
|
||||
|
||||
# Method 2: Using convenience function
|
||||
gemini_key = get_gemini_key(self.user_id)
|
||||
|
||||
if not gemini_key:
|
||||
raise ValueError(f"No Gemini API key found for user {self.user_id}")
|
||||
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
prompt = f"Write a blog section for '{section_heading}'\n\nContext: {context}"
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return response.text
|
||||
```
|
||||
|
||||
**Usage in FastAPI:**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.blog_writer_service import BlogWriterService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/api/blog/outline")
|
||||
async def create_blog_outline(
|
||||
topic: str,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
user_id = current_user.get('user_id')
|
||||
|
||||
# Create service instance with user_id
|
||||
blog_service = BlogWriterService(user_id)
|
||||
|
||||
# Service automatically uses this user's API keys
|
||||
outline = await blog_service.generate_blog_outline(topic)
|
||||
|
||||
return outline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Example 2: Research Service with Multiple APIs**
|
||||
|
||||
**File: `backend/services/research_service.py`**
|
||||
|
||||
```python
|
||||
from services.user_api_key_context import user_api_keys
|
||||
from exa_py import Exa
|
||||
import google.generativeai as genai
|
||||
|
||||
class ResearchService:
|
||||
"""
|
||||
Service for conducting research using user-specific API keys.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
|
||||
async def conduct_research(self, query: str) -> dict:
|
||||
"""
|
||||
Conduct research using both Exa (search) and Gemini (analysis).
|
||||
Uses user-specific API keys for both services.
|
||||
"""
|
||||
|
||||
with user_api_keys(self.user_id) as keys:
|
||||
exa_key = keys.get('exa')
|
||||
gemini_key = keys.get('gemini')
|
||||
|
||||
if not exa_key or not gemini_key:
|
||||
raise ValueError(f"Missing required API keys for user {self.user_id}")
|
||||
|
||||
# 1. Search using user's Exa API key
|
||||
exa = Exa(api_key=exa_key)
|
||||
search_results = exa.search_and_contents(
|
||||
query,
|
||||
num_results=5,
|
||||
text=True
|
||||
)
|
||||
|
||||
# 2. Analyze results using user's Gemini API key
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
# Prepare context from search results
|
||||
context = "\n\n".join([
|
||||
f"Source: {r.url}\n{r.text[:500]}..."
|
||||
for r in search_results.results
|
||||
])
|
||||
|
||||
prompt = f"""
|
||||
Analyze the following research results for query: "{query}"
|
||||
|
||||
{context}
|
||||
|
||||
Provide a comprehensive summary and key insights.
|
||||
"""
|
||||
|
||||
analysis = model.generate_content(prompt)
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"sources": [r.url for r in search_results.results],
|
||||
"analysis": analysis.text,
|
||||
"user_id": self.user_id # For debugging
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Example 3: Persona Generation Service**
|
||||
|
||||
**File: `backend/services/persona/core_persona_service.py`**
|
||||
|
||||
```python
|
||||
from services.user_api_key_context import user_api_keys, get_gemini_key
|
||||
import google.generativeai as genai
|
||||
from typing import Optional
|
||||
|
||||
class CorePersonaService:
|
||||
"""
|
||||
Service for generating AI writing personas.
|
||||
"""
|
||||
|
||||
def generate_core_persona(
|
||||
self,
|
||||
onboarding_data: dict,
|
||||
user_id: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Generate core persona using user's Gemini API key.
|
||||
|
||||
Args:
|
||||
onboarding_data: User's onboarding information
|
||||
user_id: User ID (optional - uses .env in dev mode if None)
|
||||
"""
|
||||
|
||||
# Get user-specific Gemini key
|
||||
# In dev mode (user_id=None), this uses .env
|
||||
# In prod mode, this fetches from database
|
||||
gemini_key = get_gemini_key(user_id)
|
||||
|
||||
if not gemini_key:
|
||||
if user_id:
|
||||
raise ValueError(f"No Gemini API key found for user {user_id}")
|
||||
else:
|
||||
raise ValueError("No Gemini API key found in .env file")
|
||||
|
||||
# Configure Gemini
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
# Extract user's business info
|
||||
business_data = onboarding_data.get('businessData', {})
|
||||
website_analysis = onboarding_data.get('websiteAnalysis', {})
|
||||
|
||||
prompt = f"""
|
||||
Generate an AI writing persona based on:
|
||||
|
||||
Business: {business_data.get('name')}
|
||||
Industry: {business_data.get('industry')}
|
||||
Tone: {website_analysis.get('tone')}
|
||||
|
||||
Create a detailed writing persona including voice, style, and personality.
|
||||
"""
|
||||
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return {
|
||||
"persona": response.text,
|
||||
"user_id": user_id,
|
||||
"source": "dev_env" if user_id is None else "user_database"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Example 4: Background Task with User Keys**
|
||||
|
||||
**File: `backend/services/async_content_generator.py`**
|
||||
|
||||
```python
|
||||
from fastapi import BackgroundTasks
|
||||
from services.user_api_key_context import user_api_keys
|
||||
import google.generativeai as genai
|
||||
|
||||
async def generate_content_background(
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
prompt: str,
|
||||
callback_url: str = None
|
||||
):
|
||||
"""
|
||||
Background task that generates content using user's API keys.
|
||||
This runs asynchronously and doesn't block the API response.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Get user-specific API keys
|
||||
with user_api_keys(user_id) as keys:
|
||||
gemini_key = keys.get('gemini')
|
||||
|
||||
if not gemini_key:
|
||||
# Log error and notify user
|
||||
logger.error(f"No Gemini API key for user {user_id} in task {task_id}")
|
||||
return
|
||||
|
||||
# Configure Gemini
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
|
||||
# Generate content (this may take a while)
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
# Save to database or send callback
|
||||
if callback_url:
|
||||
# Notify user that content is ready
|
||||
await send_callback(callback_url, {
|
||||
"task_id": task_id,
|
||||
"content": response.text,
|
||||
"status": "completed"
|
||||
})
|
||||
|
||||
logger.info(f"Task {task_id} completed for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Task {task_id} failed for user {user_id}: {e}")
|
||||
|
||||
|
||||
# Usage in FastAPI endpoint
|
||||
@router.post("/api/generate-async")
|
||||
async def generate_async(
|
||||
prompt: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
user_id = current_user.get('user_id')
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
# Queue background task
|
||||
background_tasks.add_task(
|
||||
generate_content_background,
|
||||
user_id=user_id,
|
||||
task_id=task_id,
|
||||
prompt=prompt
|
||||
)
|
||||
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": "queued",
|
||||
"message": "Content generation started"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Example 5: Migrating Existing Service**
|
||||
|
||||
**Before (WRONG - uses global .env):**
|
||||
|
||||
```python
|
||||
import os
|
||||
import google.generativeai as genai
|
||||
|
||||
class OldBlogService:
|
||||
def generate_content(self, prompt: str):
|
||||
# BAD: Uses same API key for all users!
|
||||
gemini_key = os.getenv('GEMINI_API_KEY')
|
||||
genai.configure(api_key=gemini_key)
|
||||
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return response.text
|
||||
```
|
||||
|
||||
**After (CORRECT - uses user-specific keys):**
|
||||
|
||||
```python
|
||||
from services.user_api_key_context import user_api_keys
|
||||
import google.generativeai as genai
|
||||
|
||||
class NewBlogService:
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
|
||||
def generate_content(self, prompt: str):
|
||||
# GOOD: Uses user-specific API key!
|
||||
with user_api_keys(self.user_id) as keys:
|
||||
gemini_key = keys.get('gemini')
|
||||
|
||||
if not gemini_key:
|
||||
raise ValueError(f"No Gemini API key for user {self.user_id}")
|
||||
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel('gemini-pro')
|
||||
response = model.generate_content(prompt)
|
||||
|
||||
return response.text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ **DO:**
|
||||
|
||||
1. **Always pass `user_id` to services:**
|
||||
```python
|
||||
service = BlogWriterService(user_id=current_user.get('user_id'))
|
||||
```
|
||||
|
||||
2. **Use context manager for multiple keys:**
|
||||
```python
|
||||
with user_api_keys(user_id) as keys:
|
||||
gemini_key = keys.get('gemini')
|
||||
exa_key = keys.get('exa')
|
||||
```
|
||||
|
||||
3. **Check for missing keys:**
|
||||
```python
|
||||
if not gemini_key:
|
||||
raise HTTPException(status_code=400, detail="Please configure your Gemini API key")
|
||||
```
|
||||
|
||||
4. **Log which user's keys are being used:**
|
||||
```python
|
||||
logger.info(f"Generating content for user {user_id} with their API keys")
|
||||
```
|
||||
|
||||
### ❌ **DON'T:**
|
||||
|
||||
1. **Don't use `os.getenv()` directly:**
|
||||
```python
|
||||
# WRONG - same key for all users!
|
||||
gemini_key = os.getenv('GEMINI_API_KEY')
|
||||
```
|
||||
|
||||
2. **Don't forget to pass `user_id`:**
|
||||
```python
|
||||
# WRONG - will use .env even in production!
|
||||
with user_api_keys() as keys: # Missing user_id!
|
||||
```
|
||||
|
||||
3. **Don't hardcode API keys:**
|
||||
```python
|
||||
# WRONG - security risk!
|
||||
genai.configure(api_key="AIzaSy...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### **Test in Development:**
|
||||
|
||||
```python
|
||||
# Set DEBUG=true in backend/.env
|
||||
# Then test:
|
||||
|
||||
def test_dev_mode():
|
||||
# user_id=None should use .env file
|
||||
with user_api_keys(user_id=None) as keys:
|
||||
assert keys.get('gemini') == os.getenv('GEMINI_API_KEY')
|
||||
```
|
||||
|
||||
### **Test in Production:**
|
||||
|
||||
```python
|
||||
# Set DEBUG=false and DEPLOY_ENV=render
|
||||
# Then test:
|
||||
|
||||
def test_prod_mode():
|
||||
# Should fetch from database
|
||||
user_id = "user_12345"
|
||||
with user_api_keys(user_id) as keys:
|
||||
# Keys should come from database, not .env
|
||||
assert keys.get('gemini') != os.getenv('GEMINI_API_KEY')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Method | Use Case | Example |
|
||||
|--------|----------|---------|
|
||||
| `user_api_keys(user_id)` | Multiple keys needed | Research service (Exa + Gemini) |
|
||||
| `get_gemini_key(user_id)` | Single key needed | Blog writer (only Gemini) |
|
||||
| `get_exa_key(user_id)` | Single key needed | Search service (only Exa) |
|
||||
| `get_user_api_keys(user_id)` | FastAPI dependency | Endpoint that needs all keys |
|
||||
|
||||
**Key Principle:**
|
||||
> Always pass `user_id` to get user-specific API keys. In development (`user_id=None`), it uses `.env` for convenience.
|
||||
|
||||
This ensures:
|
||||
- ✅ **Local dev**: Your keys from `.env`
|
||||
- ✅ **Production**: Each user's keys from database
|
||||
- ✅ **Zero cost**: Alpha testers use their own API keys
|
||||
- ✅ **User isolation**: No conflicts between users
|
||||
|
||||
Reference in New Issue
Block a user