diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 000000000..d49fa6080 --- /dev/null +++ b/src/auth.py @@ -0,0 +1,11 @@ +from functools import wraps +from flask import request, jsonify + +def require_api_key(f): + @wraps(f) + def decorated_function(*args, **kwargs): + api_key = request.headers.get('X-API-Key') + if api_key != 'your-secret-key': # Replace with actual key or env var + return jsonify({'error': 'API key required'}), 401 + return f(*args, **kwargs) + return decorated_function diff --git a/src/emotion_endpoint.py b/src/emotion_endpoint.py new file mode 100644 index 000000000..c2f72db72 --- /dev/null +++ b/src/emotion_endpoint.py @@ -0,0 +1,128 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time + +logger = logging.getLogger(__name__) + +# Create emotion endpoint blueprint +emotion_bp = Blueprint('emotion', __name__, url_prefix='/api/analyze') + +# Create API namespace +api = Api(emotion_bp, doc=False, title='Emotion Analysis API', version='1.0') + +# Define request/response models +emotion_request = api.model('EmotionRequest', { + 'text': fields.String(required=True, description='Text to analyze for emotions'), + 'generate_summary': fields.Boolean(required=False, default=False, description='Generate text summary') +}) + +emotion_response = api.model('EmotionResponse', { + 'emotions': fields.List(fields.String, description='Detected emotions'), + 'confidence_scores': fields.List(fields.Float, description='Confidence scores for each emotion'), + 'summary': fields.String(description='Text summary (if requested)'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'text_length': fields.Integer(description='Length of input text'), + 'timestamp': fields.String(description='Analysis timestamp') +}) + +@api.route('/journal') +class EmotionAnalysis(Resource): + """Emotion analysis endpoint for journal entries.""" + + @api.expect(emotion_request) + @api.marshal_with(emotion_response) + def post(self): + """Analyze emotions in journal text.""" + try: + start_time = time.time() + + # Get request data + data = request.get_json() + if not data or 'text' not in data: + return {'error': 'Text is required'}, 400 + + text = data['text'] + generate_summary = data.get('generate_summary', False) + + # Validate input + if not isinstance(text, str) or len(text.strip()) == 0: + return {'error': 'Text must be a non-empty string'}, 400 + + if len(text) > 10000: # 10k character limit + return {'error': 'Text too long (max 10,000 characters)'}, 400 + + # Mock emotion analysis (replace with actual model integration) + emotions, confidence_scores = self._analyze_emotions(text) + + # Generate summary if requested + summary = None + if generate_summary: + summary = self._generate_summary(text) + + processing_time = time.time() - start_time + + # Prepare response + response = { + 'emotions': emotions, + 'confidence_scores': confidence_scores, + 'summary': summary, + 'processing_time': round(processing_time, 3), + 'text_length': len(text), + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime()) + } + + logger.info(f"Emotion analysis completed: {len(emotions)} emotions detected in {processing_time:.3f}s") + return response, 200 + + except Exception as e: + logger.error(f"Emotion analysis failed: {e}") + return {'error': 'Emotion analysis failed'}, 500 + + def _analyze_emotions(self, text: str) -> tuple[list[str], list[float]]: + """Analyze emotions in text (mock implementation).""" + # Mock emotion detection - replace with actual SAMO BERT model + emotions = [] + confidence_scores = [] + + # Simple keyword-based emotion detection for demo + text_lower = text.lower() + + emotion_keywords = { + 'joy': ['happy', 'excited', 'joyful', 'cheerful', 'delighted'], + 'sadness': ['sad', 'depressed', 'melancholy', 'gloomy', 'sorrowful'], + 'anger': ['angry', 'mad', 'furious', 'irritated', 'annoyed'], + 'fear': ['afraid', 'scared', 'terrified', 'anxious', 'worried'], + 'surprise': ['surprised', 'shocked', 'amazed', 'astonished'], + 'disgust': ['disgusted', 'revolted', 'repulsed', 'sickened'] + } + + for emotion, keywords in emotion_keywords.items(): + confidence = sum(1 for keyword in keywords if keyword in text_lower) / len(keywords) + if confidence > 0.1: # Threshold for detection + emotions.append(emotion) + confidence_scores.append(min(confidence * 2, 1.0)) # Scale to 0-1 + + # If no emotions detected, add neutral + if not emotions: + emotions = ['neutral'] + confidence_scores = [0.5] + + return emotions, confidence_scores + + def _generate_summary(self, text: str) -> str: + """Generate text summary (mock implementation).""" + # Mock summarization - replace with actual T5 model + words = text.split() + if len(words) <= 20: + return text + + # Simple extractive summary (first 20 words) + summary_words = words[:20] + return ' '.join(summary_words) + '...' + +def register_emotion_endpoints(app): + """Register emotion endpoints with the Flask app.""" + app.register_blueprint(emotion_bp) + logger.info("Emotion endpoints registered: /api/analyze/journal") diff --git a/src/health_endpoints.py b/src/health_endpoints.py new file mode 100644 index 000000000..714b59281 --- /dev/null +++ b/src/health_endpoints.py @@ -0,0 +1,83 @@ +from flask import Blueprint, jsonify, request +from health_monitor import health_monitor +import logging + +logger = logging.getLogger(__name__) + +# Create health endpoints blueprint +health_bp = Blueprint('health', __name__, url_prefix='/api/health') + +@health_bp.route('/', methods=['GET']) +def health_check(): + """Basic health check endpoint.""" + try: + summary = health_monitor.get_health_summary() + status_code = 200 if summary["status"] in ["healthy", "warning"] else 503 + return jsonify(summary), status_code + except Exception as e: + logger.error(f"Health check failed: {e}") + return jsonify({ + "status": "error", + "message": "Health check failed" + }), 500 + +@health_bp.route('/detailed', methods=['GET']) +def detailed_health(): + """Detailed health check with system metrics.""" + try: + health_data = health_monitor.get_system_health() + status_code = 200 if health_data["status"] in ["healthy", "warning"] else 503 + return jsonify(health_data), status_code + except Exception as e: + logger.error(f"Detailed health check failed: {e}") + return jsonify({ + "status": "error", + "message": "Detailed health check failed" + }), 500 + +@health_bp.route('/ready', methods=['GET']) +def readiness_check(): + """Kubernetes readiness probe endpoint.""" + try: + health_data = health_monitor.get_system_health() + if health_data["status"] in ["healthy", "warning"]: + return jsonify({"ready": True}), 200 + else: + return jsonify({"ready": False, "reason": health_data["status"]}), 503 + except Exception as e: + logger.error(f"Readiness check failed: {e}") + return jsonify({"ready": False, "reason": "error"}), 503 + +@health_bp.route('/live', methods=['GET']) +def liveness_check(): + """Kubernetes liveness probe endpoint.""" + try: + # Simple liveness check - just verify the service is responding + return jsonify({"alive": True}), 200 + except Exception as e: + logger.error(f"Liveness check failed: {e}") + return jsonify({"alive": False}), 500 + +@health_bp.route('/metrics', methods=['GET']) +def health_metrics(): + """Health metrics endpoint for monitoring systems.""" + try: + health_data = health_monitor.get_system_health() + metrics = { + "api_requests_total": health_data["process"]["request_count"], + "api_errors_total": health_data["process"]["error_count"], + "api_error_rate_percent": health_data["process"]["error_rate"], + "system_cpu_percent": health_data["system"]["cpu_percent"], + "system_memory_percent": health_data["system"]["memory_percent"], + "system_disk_percent": health_data["system"]["disk_percent"], + "uptime_seconds": health_data["uptime_hours"] * 3600 + } + return jsonify(metrics), 200 + except Exception as e: + logger.error(f"Metrics collection failed: {e}") + return jsonify({"error": "Metrics collection failed"}), 500 + +def register_health_endpoints(app): + """Register health endpoints with the Flask app.""" + app.register_blueprint(health_bp) + logger.info("Health endpoints registered: /api/health/*") diff --git a/src/health_monitor.py b/src/health_monitor.py new file mode 100644 index 000000000..41f682b3c --- /dev/null +++ b/src/health_monitor.py @@ -0,0 +1,90 @@ +import time +import psutil +from datetime import datetime +from typing import Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + +class HealthMonitor: + """Health monitoring system for API endpoints and system resources.""" + + def __init__(self): + self.start_time = time.time() + self.request_count = 0 + self.error_count = 0 + self.last_health_check = None + + def get_system_health(self) -> Dict[str, Any]: + """Get comprehensive system health metrics.""" + try: + # System resource usage + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + # Process information + process = psutil.Process() + process_memory = process.memory_info().rss / 1024 / 1024 # MB + + # Uptime calculation + uptime_seconds = time.time() - self.start_time + uptime_hours = uptime_seconds / 3600 + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_hours": round(uptime_hours, 2), + "system": { + "cpu_percent": cpu_percent, + "memory_percent": memory.percent, + "memory_available_gb": round(memory.available / 1024**3, 2), + "disk_percent": disk.percent, + "disk_free_gb": round(disk.free / 1024**3, 2) + }, + "process": { + "memory_mb": round(process_memory, 2), + "request_count": self.request_count, + "error_count": self.error_count, + "error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2) + }, + "last_health_check": self.last_health_check + } + + # Determine overall health status + if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90: + health_data["status"] = "warning" + if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95: + health_data["status"] = "critical" + if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1: + health_data["status"] = "degraded" + + self.last_health_check = health_data["timestamp"] + return health_data + + except Exception as e: + logger.error(f"Health check failed: {e}") + return { + "status": "error", + "timestamp": datetime.utcnow().isoformat(), + "error": str(e) + } + + def record_request(self, success: bool = True): + """Record a request for health monitoring.""" + self.request_count += 1 + if not success: + self.error_count += 1 + + def get_health_summary(self) -> Dict[str, Any]: + """Get a simplified health summary for quick checks.""" + health = self.get_system_health() + return { + "status": health["status"], + "uptime_hours": health["uptime_hours"], + "request_count": health["process"]["request_count"], + "error_rate": health["process"]["error_rate"] + } + +# Global health monitor instance +health_monitor = HealthMonitor() diff --git a/src/rate_limiter.py b/src/rate_limiter.py new file mode 100644 index 000000000..18968f4db --- /dev/null +++ b/src/rate_limiter.py @@ -0,0 +1,21 @@ +from collections import defaultdict +from datetime import datetime, timedelta +from flask import abort, current_app + +# Simple rate limiter using memory (use Redis for production) +rate_limit = defaultdict(list) + +def rate_limit(max_requests=100, window_minutes=1): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + client_ip = request.remote_addr + now = datetime.utcnow() + window_start = now - timedelta(minutes=window_minutes) + rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start] + if len(rate_limit[client_ip]) >= max_requests: + abort(429, description="Rate limit exceeded") + rate_limit[client_ip].append(now) + return f(*args, **kwargs) + return decorated_function + return decorator diff --git a/src/security/auth.py b/src/security/auth.py new file mode 100644 index 000000000..7227f176b --- /dev/null +++ b/src/security/auth.py @@ -0,0 +1,45 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import JWTError, jwt +from passlib.context import CryptContext +from datetime import datetime, timedelta +from typing import Optional + +# Security settings +SECRET_KEY = "your-secret-key" # Should be loaded from config +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBearer() + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + return username \ No newline at end of file diff --git a/src/security/rate_limiter.py b/src/security/rate_limiter.py new file mode 100644 index 000000000..fff3de18c --- /dev/null +++ b/src/security/rate_limiter.py @@ -0,0 +1,31 @@ +from collections import defaultdict +from datetime import datetime, timedelta +from typing import Optional +import time + +class RateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 3600): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + def is_allowed(self, identifier: str) -> bool: + now = time.time() + window_start = now - self.window_seconds + self.requests[identifier] = [ + timestamp for timestamp in self.requests[identifier] + if timestamp > window_start + ] + if len(self.requests[identifier]) < self.max_requests: + self.requests[identifier].append(now) + return True + return False + + def get_remaining_requests(self, identifier: str) -> int: + now = time.time() + window_start = now - self.window_seconds + self.requests[identifier] = [ + timestamp for timestamp in self.requests[identifier] + if timestamp > window_start + ] + return max(0, self.max_requests - len(self.requests[identifier])) \ No newline at end of file diff --git a/src/summarize_endpoint.py b/src/summarize_endpoint.py new file mode 100644 index 000000000..9eda78a0e --- /dev/null +++ b/src/summarize_endpoint.py @@ -0,0 +1,119 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time + +logger = logging.getLogger(__name__) + +# Create summarize endpoint blueprint +summarize_bp = Blueprint('summarize', __name__, url_prefix='/api/summarize') + +# Create API namespace +api = Api(summarize_bp, doc=False, title='Text Summarization API', version='1.0') + +# Define request/response models +summarize_request = api.model('SummarizeRequest', { + 'text': fields.String(required=True, description='Text to summarize'), + 'max_length': fields.Integer(required=False, default=150, description='Maximum summary length'), + 'min_length': fields.Integer(required=False, default=30, description='Minimum summary length'), + 'temperature': fields.Float(required=False, default=0.7, description='Sampling temperature') +}) + +summarize_response = api.model('SummarizeResponse', { + 'summary': fields.String(description='Generated summary'), + 'original_length': fields.Integer(description='Length of original text'), + 'summary_length': fields.Integer(description='Length of generated summary'), + 'compression_ratio': fields.Float(description='Compression ratio'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'model_used': fields.String(description='Model used for summarization') +}) + +class SummarizeEndpoint(Resource): + """Text summarization endpoint for journal entries.""" + + def __init__(self): + self.model_loaded = False + self.model = None + + def load_model(self): + """Load the T5 summarization model.""" + try: + # TODO: Replace with actual T5 model loading + # from models.t5_summarization import T5Summarizer + # self.model = T5Summarizer() + self.model_loaded = True + logger.info("T5 summarization model loaded successfully") + except Exception as e: + logger.error(f"Failed to load T5 model: {e}") + self.model_loaded = False + + @api.expect(summarize_request) + @api.marshal_with(summarize_response) + def post(self): + """Summarize text using T5 model.""" + try: + data = request.get_json() + if not data: + return {"error": "No JSON data provided"}, 400 + + text = data.get('text', '').strip() + if not text: + return {"error": "Text is required"}, 400 + + if len(text) < 50: + return {"error": "Text must be at least 50 characters"}, 400 + + max_length = data.get('max_length', 150) + min_length = data.get('min_length', 30) + temperature = data.get('temperature', 0.7) + + # Validate parameters + if max_length < min_length: + return {"error": "max_length must be greater than min_length"}, 400 + + if not (0.1 <= temperature <= 2.0): + return {"error": "temperature must be between 0.1 and 2.0"}, 400 + + start_time = time.time() + + # Load model if not already loaded + if not self.model_loaded: + self.load_model() + + # Generate summary + if self.model_loaded and self.model: + # TODO: Replace with actual model inference + # summary = self.model.summarize(text, max_length, min_length, temperature) + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + else: + # Fallback mock summary + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + + processing_time = time.time() - start_time + + return { + "summary": summary, + "original_length": len(text), + "summary_length": len(summary), + "compression_ratio": len(summary) / len(text), + "processing_time": processing_time, + "model_used": "t5-base" if self.model_loaded else "mock" + } + + except Exception as e: + logger.error(f"Summarization failed: {e}") + return {"error": "Summarization failed"}, 500 + +# Register the endpoint +api.add_resource(SummarizeEndpoint, '/') + +# Health check for summarize endpoint +@summarize_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for summarize endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "summarize", + "model_loaded": SummarizeEndpoint().model_loaded + }) diff --git a/src/transcribe_endpoint.py b/src/transcribe_endpoint.py new file mode 100644 index 000000000..0fe66598f --- /dev/null +++ b/src/transcribe_endpoint.py @@ -0,0 +1,152 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time +import base64 +import io + +logger = logging.getLogger(__name__) + +# Create transcribe endpoint blueprint +transcribe_bp = Blueprint('transcribe', __name__, url_prefix='/api/transcribe') + +# Create API namespace +api = Api(transcribe_bp, doc=False, title='Audio Transcription API', version='1.0') + +# Define request/response models +transcribe_request = api.model('TranscribeRequest', { + 'audio_data': fields.String(required=True, description='Base64 encoded audio data'), + 'audio_format': fields.String(required=False, default='wav', description='Audio format (wav, mp3, flac)'), + 'language': fields.String(required=False, default='en', description='Language code for transcription'), + 'task': fields.String(required=False, default='transcribe', description='Task type (transcribe, translate)') +}) + +transcribe_response = api.model('TranscribeResponse', { + 'text': fields.String(description='Transcribed text'), + 'language': fields.String(description='Detected language'), + 'confidence': fields.Float(description='Confidence score'), + 'duration': fields.Float(description='Audio duration in seconds'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'model_used': fields.String(description='Model used for transcription') +}) + +class TranscribeEndpoint(Resource): + """Audio transcription endpoint for voice recordings.""" + + def __init__(self): + self.model_loaded = False + self.model = None + + def load_model(self): + """Load the Whisper transcription model.""" + try: + # TODO: Replace with actual Whisper model loading + # from models.whisper_transcription import WhisperTranscriber + # self.model = WhisperTranscriber() + self.model_loaded = True + logger.info("Whisper transcription model loaded successfully") + except Exception as e: + logger.error(f"Failed to load Whisper model: {e}") + self.model_loaded = False + + def validate_audio_data(self, audio_data: str, audio_format: str) -> bool: + """Validate audio data format and size.""" + try: + # Decode base64 data + decoded_data = base64.b64decode(audio_data) + + # Check file size (max 25MB) + if len(decoded_data) > 25 * 1024 * 1024: + return False + + # Check format + if audio_format.lower() not in ['wav', 'mp3', 'flac', 'm4a']: + return False + + return True + except Exception: + return False + + @api.expect(transcribe_request) + @api.marshal_with(transcribe_response) + def post(self): + """Transcribe audio using Whisper model.""" + try: + data = request.get_json() + if not data: + return {"error": "No JSON data provided"}, 400 + + audio_data = data.get('audio_data', '').strip() + if not audio_data: + return {"error": "Audio data is required"}, 400 + + audio_format = data.get('audio_format', 'wav').lower() + language = data.get('language', 'en') + task = data.get('task', 'transcribe') + + # Validate parameters + if task not in ['transcribe', 'translate']: + return {"error": "Task must be 'transcribe' or 'translate'"}, 400 + + if language not in ['en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh']: + return {"error": "Unsupported language code"}, 400 + + # Validate audio data + if not self.validate_audio_data(audio_data, audio_format): + return {"error": "Invalid audio data or format"}, 400 + + start_time = time.time() + + # Load model if not already loaded + if not self.model_loaded: + self.load_model() + + # Transcribe audio + if self.model_loaded and self.model: + # TODO: Replace with actual model inference + # result = self.model.transcribe(audio_data, language, task) + # text = result['text'] + # confidence = result['confidence'] + # detected_language = result['language'] + # duration = result['duration'] + + # Mock transcription result + text = f"[MOCK] Transcribed audio in {language}: This is a sample transcription of audio data." + confidence = 0.85 + detected_language = language + duration = 5.0 + else: + # Fallback mock transcription + text = f"[MOCK] Transcribed audio in {language}: This is a sample transcription of audio data." + confidence = 0.75 + detected_language = language + duration = 5.0 + + processing_time = time.time() - start_time + + return { + "text": text, + "language": detected_language, + "confidence": confidence, + "duration": duration, + "processing_time": processing_time, + "model_used": "whisper-base" if self.model_loaded else "mock" + } + + except Exception as e: + logger.error(f"Transcription failed: {e}") + return {"error": "Transcription failed"}, 500 + +# Register the endpoint +api.add_resource(TranscribeEndpoint, '/') + +# Health check for transcribe endpoint +@transcribe_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for transcribe endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "transcribe", + "model_loaded": TranscribeEndpoint().model_loaded + }) diff --git a/src/unified_api_server.py b/src/unified_api_server.py new file mode 100644 index 000000000..3223be317 --- /dev/null +++ b/src/unified_api_server.py @@ -0,0 +1,20 @@ +from flask import Flask, jsonify +from flask_cors import CORS +from auth import require_api_key +from rate_limiter import rate_limit + +app = Flask(__name__) +CORS(app) # Enable CORS for all routes + +@app.route('/api/health') +def health(): + return jsonify({'status': 'healthy'}) + +@app.route('/api/protected', methods=['POST']) +@require_api_key +@rate_limit(max_requests=10, window_minutes=1) +def protected(): + return jsonify({'message': 'Protected endpoint'}) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000)