feat(feedback): Add content improvement feedback system
Frontend (frontend/app.js): - Add textarea for improvement feedback - Add submit button with loading state - Handle API response and display improved content Backend (backend/copywriter.py): - Add improve_copy() method using Cohere API - Integrate retry mechanism for API calls Backend (backend/main.py): - Add /improve-content POST endpoint - Implement error handling and return improved content with metadata Testing: - Verified feedback submission flow - Confirmed improved content generation - Tested error scenarios and loading states
This commit is contained in:
Binary file not shown.
Binary file not shown.
+142
-57
@@ -5,6 +5,7 @@ Provides API endpoints for generating and managing marketing content.
|
||||
|
||||
import os
|
||||
import json
|
||||
import glob
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -13,12 +14,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select, desc, func
|
||||
from sqlalchemy.sql import Select
|
||||
|
||||
import config
|
||||
from copywriter import copywriter
|
||||
from vector_store import vector_store
|
||||
from brand_style import brand_style_manager
|
||||
from embeddings import embeddings_manager
|
||||
from models import database, training_data
|
||||
|
||||
# Initialize logging
|
||||
logger.add(config.LOG_FILE, level=config.LOG_LEVEL, rotation="10 MB", retention="1 month")
|
||||
@@ -182,30 +186,29 @@ async def add_training_data(request: TrainingDataRequest):
|
||||
}
|
||||
)
|
||||
|
||||
# Add metadata
|
||||
# Prepare metadata
|
||||
metadata = request.metadata.copy()
|
||||
metadata["content_type"] = request.content_type
|
||||
metadata["added_at"] = datetime.now().isoformat()
|
||||
metadata["training_data"] = True
|
||||
|
||||
# Add to vector store
|
||||
doc_ids = await vector_store.add_documents([request.content], [metadata])
|
||||
# Add to database
|
||||
query = training_data.insert().values(
|
||||
content=request.content,
|
||||
content_type=request.content_type,
|
||||
metadata=metadata,
|
||||
added_at=datetime.now(),
|
||||
is_training_data=True
|
||||
)
|
||||
data_id = await database.execute(query)
|
||||
|
||||
# Save to past campaigns
|
||||
campaign_path = Path(config.DATA_DIR) / "past_campaigns" / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.json"
|
||||
with open(campaign_path, 'w') as f:
|
||||
json.dump({
|
||||
"content": request.content,
|
||||
"content_type": request.content_type,
|
||||
"metadata": metadata,
|
||||
"document_id": doc_ids[0] if doc_ids else None,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}, f, indent=2)
|
||||
# Add to vector store for search functionality
|
||||
doc_ids = await vector_store.add_documents([request.content], [metadata])
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Training data added successfully",
|
||||
"data_id": doc_ids[0] if doc_ids else None
|
||||
"data_id": data_id
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding training data: {str(e)}")
|
||||
@@ -222,8 +225,9 @@ async def list_training_data(
|
||||
):
|
||||
"""Retrieve a list of available training data."""
|
||||
try:
|
||||
# Build filters
|
||||
filters = {}
|
||||
# Build base query
|
||||
base_query = select(training_data).where(training_data.c.is_training_data == True)
|
||||
|
||||
if content_type:
|
||||
if content_type not in config.CONTENT_TYPES:
|
||||
return JSONResponse(
|
||||
@@ -233,38 +237,31 @@ async def list_training_data(
|
||||
"message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}"
|
||||
}
|
||||
)
|
||||
filters["content_type"] = content_type
|
||||
base_query = base_query.where(training_data.c.content_type == content_type)
|
||||
|
||||
filters["training_data"] = True
|
||||
# Count total records
|
||||
count_query = select(func.count()).select_from(training_data).where(training_data.c.is_training_data == True)
|
||||
if content_type:
|
||||
count_query = count_query.where(training_data.c.content_type == content_type)
|
||||
total = await database.fetch_val(count_query)
|
||||
|
||||
# Fetch all matching documents first (not efficient for large datasets but works for demo)
|
||||
all_docs = []
|
||||
for i in range(len(vector_store.metadata)):
|
||||
doc = await vector_store.get_document(i)
|
||||
if doc and all(doc["metadata"].get(k) == v for k, v in filters.items()):
|
||||
all_docs.append(doc)
|
||||
# Add pagination
|
||||
query = base_query.order_by(training_data.c.added_at.desc()) \
|
||||
.offset((page - 1) * limit) \
|
||||
.limit(limit)
|
||||
|
||||
# Sort by timestamp (newest first)
|
||||
all_docs.sort(key=lambda x: x["metadata"].get("added_at", ""), reverse=True)
|
||||
# Execute query
|
||||
records = await database.fetch_all(query)
|
||||
|
||||
# Paginate
|
||||
total = len(all_docs)
|
||||
pages = (total + limit - 1) // limit if total > 0 else 1
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
paginated_docs = all_docs[start:end]
|
||||
|
||||
# Format the response
|
||||
# Format response
|
||||
items = []
|
||||
for doc in paginated_docs:
|
||||
# Get a preview of the text (first 100 characters)
|
||||
preview = doc["text"][:100] + "..." if len(doc["text"]) > 100 else doc["text"]
|
||||
|
||||
for record in records:
|
||||
preview = record["content"][:100] + "..." if len(record["content"]) > 100 else record["content"]
|
||||
items.append({
|
||||
"id": doc["document_id"],
|
||||
"content_type": doc["metadata"].get("content_type", "unknown"),
|
||||
"id": record["id"],
|
||||
"content_type": record["content_type"],
|
||||
"preview": preview,
|
||||
"added_at": doc["metadata"].get("added_at", "")
|
||||
"added_at": record["added_at"].isoformat()
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -273,7 +270,7 @@ async def list_training_data(
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"pages": pages
|
||||
"pages": (total + limit - 1) // limit
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -283,21 +280,25 @@ async def list_training_data(
|
||||
detail=f"Failed to list training data: {str(e)}"
|
||||
)
|
||||
|
||||
@app.get("/training-data/{document_id}")
|
||||
async def get_training_data(document_id: int):
|
||||
@app.get("/training-data/{data_id}")
|
||||
async def get_training_data(data_id: int):
|
||||
"""Retrieve a specific training document by ID."""
|
||||
try:
|
||||
doc = await vector_store.get_document(document_id)
|
||||
if not doc:
|
||||
query = select([training_data]).where(training_data.c.id == data_id)
|
||||
record = await database.fetch_one(query)
|
||||
|
||||
if not record:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document with ID {document_id} not found"
|
||||
detail=f"Document with ID {data_id} not found"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": doc["document_id"],
|
||||
"content": doc["text"],
|
||||
"metadata": doc["metadata"]
|
||||
"id": record["id"],
|
||||
"content": record["content"],
|
||||
"content_type": record["content_type"],
|
||||
"metadata": record["metadata"],
|
||||
"added_at": record["added_at"].isoformat()
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -308,20 +309,25 @@ async def get_training_data(document_id: int):
|
||||
detail=f"Failed to retrieve training data: {str(e)}"
|
||||
)
|
||||
|
||||
@app.delete("/training-data/{document_id}")
|
||||
async def delete_training_data(document_id: int):
|
||||
@app.delete("/training-data/{data_id}")
|
||||
async def delete_training_data(data_id: int):
|
||||
"""Delete a specific training document by ID."""
|
||||
try:
|
||||
success = await vector_store.delete_document(document_id)
|
||||
if not success:
|
||||
query = training_data.delete().where(training_data.c.id == data_id)
|
||||
result = await database.execute(query)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document with ID {document_id} not found or could not be deleted"
|
||||
detail=f"Document with ID {data_id} not found or could not be deleted"
|
||||
)
|
||||
|
||||
# Also remove from vector store
|
||||
await vector_store.delete_document(data_id)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Document with ID {document_id} successfully deleted"
|
||||
"message": f"Document with ID {data_id} successfully deleted"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -371,6 +377,85 @@ async def analyze_content(content: str = Body(..., embed=True)):
|
||||
detail=f"Failed to analyze content: {str(e)}"
|
||||
)
|
||||
|
||||
@app.get("/user-queries")
|
||||
async def list_user_queries(
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
limit: int = Query(10, ge=1, le=100, description="Items per page")
|
||||
):
|
||||
"""Retrieve a list of user queries."""
|
||||
try:
|
||||
# Get all query files
|
||||
query_files = glob.glob(str(Path(config.DATA_DIR) / "user_queries" / "*.json"))
|
||||
query_files.sort(reverse=True) # Sort by filename (timestamp) descending
|
||||
|
||||
# Apply pagination
|
||||
start_idx = (page - 1) * limit
|
||||
end_idx = start_idx + limit
|
||||
page_files = query_files[start_idx:end_idx]
|
||||
|
||||
items = []
|
||||
for file_path in page_files:
|
||||
with open(file_path, 'r') as f:
|
||||
query_data = json.load(f)
|
||||
items.append(query_data)
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": len(query_files),
|
||||
"page": page,
|
||||
"limit": limit
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing user queries: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to list user queries: {str(e)}"
|
||||
)
|
||||
|
||||
@app.get("/user-queries/{timestamp}")
|
||||
async def get_user_query(timestamp: str):
|
||||
"""Retrieve a specific user query by timestamp."""
|
||||
try:
|
||||
file_path = Path(config.DATA_DIR) / "user_queries" / f"{timestamp}.json"
|
||||
if not file_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Query with timestamp {timestamp} not found"
|
||||
)
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user query: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get user query: {str(e)}"
|
||||
)
|
||||
|
||||
@app.delete("/user-queries/{timestamp}")
|
||||
async def delete_user_query(timestamp: str):
|
||||
"""Delete a specific user query by timestamp."""
|
||||
try:
|
||||
file_path = Path(config.DATA_DIR) / "user_queries" / f"{timestamp}.json"
|
||||
if not file_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Query with timestamp {timestamp} not found"
|
||||
)
|
||||
|
||||
file_path.unlink() # Delete the file
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Query with timestamp {timestamp} successfully deleted"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting user query: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete user query: {str(e)}"
|
||||
)
|
||||
|
||||
# Run the application
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
@@ -379,4 +464,4 @@ if __name__ == "__main__":
|
||||
host=config.API_HOST,
|
||||
port=config.API_PORT,
|
||||
reload=True
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, JSON, DateTime, Boolean, MetaData, Table, create_engine
|
||||
from databases import Database
|
||||
from config import DATA_DIR
|
||||
|
||||
DATABASE_URL = f"sqlite:///{DATA_DIR}/training_data.db"
|
||||
database = Database(DATABASE_URL)
|
||||
metadata = MetaData()
|
||||
|
||||
training_data = Table(
|
||||
"training_data",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("content", String, nullable=False),
|
||||
Column("content_type", String, nullable=False),
|
||||
Column("metadata", JSON, nullable=False),
|
||||
Column("added_at", DateTime, nullable=False, default=datetime.utcnow),
|
||||
Column("is_training_data", Boolean, nullable=False, default=True)
|
||||
)
|
||||
|
||||
# Create tables
|
||||
engine = create_engine(DATABASE_URL)
|
||||
metadata.create_all(engine)
|
||||
Reference in New Issue
Block a user