update
This commit is contained in:
239
backend/routes/interests_routes.py
Normal file
239
backend/routes/interests_routes.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
User Interest Profile API routes for Munich News Daily.
|
||||
Provides endpoints to view and manage user interest profiles.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from services.interest_profiling_service import (
|
||||
get_user_interests,
|
||||
get_top_interests,
|
||||
build_interests_from_history,
|
||||
decay_user_interests,
|
||||
get_interest_statistics,
|
||||
delete_user_interests
|
||||
)
|
||||
|
||||
interests_bp = Blueprint('interests', __name__)
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/<email>', methods=['GET'])
|
||||
def get_interests(email):
|
||||
"""
|
||||
Get user interest profile.
|
||||
|
||||
Args:
|
||||
email: Email address of the user
|
||||
|
||||
Returns:
|
||||
JSON response with user interest profile
|
||||
"""
|
||||
try:
|
||||
profile = get_user_interests(email)
|
||||
|
||||
if not profile:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'User profile not found'
|
||||
}), 404
|
||||
|
||||
# Remove MongoDB _id field
|
||||
if '_id' in profile:
|
||||
del profile['_id']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'profile': profile
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/<email>/top', methods=['GET'])
|
||||
def get_top_user_interests(email):
|
||||
"""
|
||||
Get user's top interests sorted by score.
|
||||
|
||||
Query parameters:
|
||||
top_n: Number of top interests to return (default: 10)
|
||||
|
||||
Args:
|
||||
email: Email address of the user
|
||||
|
||||
Returns:
|
||||
JSON response with top categories and keywords
|
||||
"""
|
||||
try:
|
||||
top_n = request.args.get('top_n', 10, type=int)
|
||||
|
||||
top_interests = get_top_interests(email, top_n)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'email': email,
|
||||
'top_categories': [
|
||||
{'category': cat, 'score': score}
|
||||
for cat, score in top_interests['top_categories']
|
||||
],
|
||||
'top_keywords': [
|
||||
{'keyword': kw, 'score': score}
|
||||
for kw, score in top_interests['top_keywords']
|
||||
]
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/<email>/rebuild', methods=['POST'])
|
||||
def rebuild_interests(email):
|
||||
"""
|
||||
Rebuild user interest profile from click history.
|
||||
|
||||
Request body (optional):
|
||||
{
|
||||
"days_lookback": 30 // Number of days of history to analyze
|
||||
}
|
||||
|
||||
Args:
|
||||
email: Email address of the user
|
||||
|
||||
Returns:
|
||||
JSON response with rebuilt profile
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
days_lookback = data.get('days_lookback', 30)
|
||||
|
||||
# Validate days_lookback
|
||||
if not isinstance(days_lookback, int) or days_lookback < 1:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'days_lookback must be a positive integer'
|
||||
}), 400
|
||||
|
||||
profile = build_interests_from_history(email, days_lookback)
|
||||
|
||||
# Remove MongoDB _id field
|
||||
if '_id' in profile:
|
||||
del profile['_id']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Profile rebuilt from {days_lookback} days of history',
|
||||
'profile': profile
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/decay', methods=['POST'])
|
||||
def decay_interests():
|
||||
"""
|
||||
Decay interest scores for inactive users.
|
||||
|
||||
Request body (optional):
|
||||
{
|
||||
"decay_factor": 0.95, // Multiplier for scores (default: 0.95)
|
||||
"days_threshold": 7 // Only decay profiles older than N days
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON response with decay statistics
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
decay_factor = data.get('decay_factor', 0.95)
|
||||
days_threshold = data.get('days_threshold', 7)
|
||||
|
||||
# Validate parameters
|
||||
if not isinstance(decay_factor, (int, float)) or decay_factor <= 0 or decay_factor > 1:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'decay_factor must be between 0 and 1'
|
||||
}), 400
|
||||
|
||||
if not isinstance(days_threshold, int) or days_threshold < 1:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'days_threshold must be a positive integer'
|
||||
}), 400
|
||||
|
||||
result = decay_user_interests(decay_factor, days_threshold)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Decayed interests for profiles older than {days_threshold} days',
|
||||
'statistics': result
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/statistics', methods=['GET'])
|
||||
def get_statistics():
|
||||
"""
|
||||
Get statistics about user interests across all users.
|
||||
|
||||
Returns:
|
||||
JSON response with interest statistics
|
||||
"""
|
||||
try:
|
||||
stats = get_interest_statistics()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'statistics': stats
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@interests_bp.route('/api/interests/<email>', methods=['DELETE'])
|
||||
def delete_interests(email):
|
||||
"""
|
||||
Delete user interest profile (GDPR compliance).
|
||||
|
||||
Args:
|
||||
email: Email address of the user
|
||||
|
||||
Returns:
|
||||
JSON response with confirmation
|
||||
"""
|
||||
try:
|
||||
deleted = delete_user_interests(email)
|
||||
|
||||
if not deleted:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'User profile not found'
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Interest profile deleted for {email}'
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
135
backend/routes/personalization_routes.py
Normal file
135
backend/routes/personalization_routes.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Personalization API routes for Munich News Daily.
|
||||
Provides endpoints to test and preview personalized content.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from datetime import datetime, timedelta
|
||||
from database import articles_collection
|
||||
from services.personalization_service import (
|
||||
rank_articles_for_user,
|
||||
select_personalized_articles,
|
||||
get_personalization_explanation,
|
||||
get_personalization_stats
|
||||
)
|
||||
|
||||
personalization_bp = Blueprint('personalization', __name__)
|
||||
|
||||
|
||||
@personalization_bp.route('/api/personalize/preview/<email>', methods=['GET'])
|
||||
def preview_personalized_newsletter(email):
|
||||
"""
|
||||
Preview personalized newsletter for a user.
|
||||
|
||||
Query parameters:
|
||||
max_articles: Maximum articles to return (default: 10)
|
||||
hours_lookback: Hours of articles to consider (default: 24)
|
||||
|
||||
Returns:
|
||||
JSON with personalized article selection and statistics
|
||||
"""
|
||||
try:
|
||||
max_articles = request.args.get('max_articles', 10, type=int)
|
||||
hours_lookback = request.args.get('hours_lookback', 24, type=int)
|
||||
|
||||
# Get recent articles
|
||||
cutoff_date = datetime.utcnow() - timedelta(hours=hours_lookback)
|
||||
articles = list(articles_collection.find({
|
||||
'created_at': {'$gte': cutoff_date},
|
||||
'summary': {'$exists': True, '$ne': None}
|
||||
}).sort('created_at', -1))
|
||||
|
||||
|
||||
# Select personalized articles
|
||||
personalized = select_personalized_articles(
|
||||
articles,
|
||||
email,
|
||||
max_articles=max_articles
|
||||
)
|
||||
|
||||
# Get statistics
|
||||
stats = get_personalization_stats(personalized, email)
|
||||
|
||||
# Format response
|
||||
articles_response = []
|
||||
for article in personalized:
|
||||
articles_response.append({
|
||||
'title': article.get('title', ''),
|
||||
'title_en': article.get('title_en'),
|
||||
'summary': article.get('summary', ''),
|
||||
'link': article.get('link', ''),
|
||||
'category': article.get('category', 'general'),
|
||||
'keywords': article.get('keywords', []),
|
||||
'personalization_score': article.get('personalization_score', 0.0),
|
||||
'published_at': article.get('published_at', '')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'email': email,
|
||||
'articles': articles_response,
|
||||
'statistics': stats
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@personalization_bp.route('/api/personalize/explain', methods=['POST'])
|
||||
def explain_recommendation():
|
||||
"""
|
||||
Explain why an article was recommended to a user.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"article_id": "article-id-here"
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with explanation of recommendation
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'email' not in data or 'article_id' not in data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'email and article_id required'
|
||||
}), 400
|
||||
|
||||
email = data['email']
|
||||
article_id = data['article_id']
|
||||
|
||||
# Get article
|
||||
from bson import ObjectId
|
||||
article = articles_collection.find_one({'_id': ObjectId(article_id)})
|
||||
|
||||
if not article:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Article not found'
|
||||
}), 404
|
||||
|
||||
# Get user interests
|
||||
from services.interest_profiling_service import get_user_interests
|
||||
user_interests = get_user_interests(email)
|
||||
|
||||
# Generate explanation
|
||||
explanation = get_personalization_explanation(article, user_interests)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'email': email,
|
||||
'article_title': article.get('title', ''),
|
||||
'explanation': explanation
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
@@ -79,8 +79,8 @@ def track_click(tracking_id):
|
||||
"""
|
||||
Track link clicks and redirect to original article URL.
|
||||
|
||||
Logs the click event and redirects the user to the original article URL.
|
||||
Handles invalid tracking_id by redirecting to homepage.
|
||||
Logs the click event, updates user interest profile, and redirects the user
|
||||
to the original article URL. Handles invalid tracking_id by redirecting to homepage.
|
||||
Ensures redirect completes within 200ms.
|
||||
|
||||
Args:
|
||||
@@ -115,6 +115,19 @@ def track_click(tracking_id):
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Update user interest profile (Phase 3)
|
||||
subscriber_email = tracking_record.get('subscriber_email')
|
||||
keywords = tracking_record.get('keywords', [])
|
||||
category = tracking_record.get('category', 'general')
|
||||
|
||||
if subscriber_email and subscriber_email != 'anonymized':
|
||||
try:
|
||||
from services.interest_profiling_service import update_user_interests
|
||||
update_user_interests(subscriber_email, keywords, category)
|
||||
except Exception as e:
|
||||
# Don't fail the redirect if interest update fails
|
||||
print(f"Error updating user interests: {str(e)}")
|
||||
except Exception as e:
|
||||
# Log error but still redirect
|
||||
print(f"Error tracking click for {tracking_id}: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user