# server.py from flask import Flask, request, jsonify, render_template from app import EasyFarmsAssistant from conversation_manager import ConversationManager import logging from datetime import datetime import os from flask_cors import CORS # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Initialize the Flask application app = Flask(__name__) CORS(app) # --- Initialize Core Services --- try: conv_manager = ConversationManager() assistant = EasyFarmsAssistant(manager=conv_manager) logger.info("EasyFarmsAssistant and ConversationManager initialized successfully.") except Exception as e: logger.error(f"FATAL: Could not initialize services. Error: {e}") assistant = None conv_manager = None # --- Frontend Serving Route --- @app.route('/') def index(): """Serves the main chat application page (index.html).""" return render_template('index.html') # --- API Endpoints --- @app.route('/config', methods=['GET']) def get_config(): """Provides public configuration keys to the frontend.""" return jsonify({ 'imgbb_api_key': os.getenv('IMGBB_API_KEY') }) @app.route('/chat', methods=['POST']) def chat(): """ Handles incoming user messages and returns the assistant's response. Request format: { "query": "user message text", "image_url": "optional image URL", "session_id": "optional chat ID for conversation memory", "user_id": "user identifier for authentication" } Response format: { "response": "assistant response", "chat_id": "unique chat identifier", "is_new_chat": true/false, "user_message_id": 1, "assistant_message_id": 2, "total_messages": 2 } """ if not assistant: return jsonify({"error": "Assistant is not available due to an initialization error."}), 503 try: # Handle both JSON and form data if request.is_json: data = request.get_json() user_message = data.get('query') chat_id = data.get('session_id') image_url = data.get('image_url') user_id = data.get('user_id') else: # Fallback for form data (backward compatibility) data = request.form user_message = data.get('message') or data.get('query') chat_id = data.get('session_id') image_url = data.get('image_url') user_id = data.get('user_id') # Validate input if not user_message and not image_url: return jsonify({"error": "Cannot process an empty message. Provide either 'query' text or 'image_url'."}), 400 if not user_id: return jsonify({"error": "User ID is required for authentication."}), 400 # Clean up empty/null chat_id if chat_id in [None, '', 'null', 'undefined']: chat_id = None # Process the query with the assistant result = assistant.process_query( user_message=user_message or "", user_id=user_id, chat_id=chat_id, image_url=image_url ) # Handle errors from assistant if "error" in result: return jsonify({ "error": result["error"], "chat_id": result.get("chat_id"), "is_new_chat": result.get("is_new_chat", True) }), 500 # Return successful response return jsonify({ "response": result["response"], "chat_id": result["chat_id"], "is_new_chat": result["is_new_chat"], "user_message_id": result["user_message_id"], "assistant_message_id": result["assistant_message_id"], "total_messages": result["total_messages"] }) except Exception as e: logger.error(f"Error in chat endpoint: {e}") return jsonify({"error": f"Server error: {str(e)}"}), 500 @app.route('/chats', methods=['GET']) def get_all_chats(): """ Get all chat sessions with basic information for a specific user. Query parameters: - user_id: Required user identifier Response format: [ { "session_id": "chat_abc123", "title": "Crop recommendations for...", "message_count": 4, "created_at": "2024-01-01T10:00:00", "updated_at": "2024-01-01T10:05:00" } ] """ if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: user_id = request.args.get('user_id') if not user_id: return jsonify({"error": "User ID is required"}), 400 chats = assistant.get_all_chats(user_id) return jsonify(chats) except Exception as e: logger.error(f"Error fetching all chats: {e}") return jsonify({"error": "Could not fetch chat list"}), 500 @app.route('/chat//info', methods=['GET']) def get_chat_info(chat_id): """ Get information about a specific chat for a specific user. Query parameters: - user_id: Required user identifier Response format: { "session_id": "chat_abc123", "message_count": 4, "created_at": "2024-01-01T10:00:00", "updated_at": "2024-01-01T10:05:00", "exists": true } """ if not assistant: return jsonify({"error": "Assistant not available"}), 503 try: if not chat_id: return jsonify({"error": "Chat ID is required"}), 400 user_id = request.args.get('user_id') if not user_id: return jsonify({"error": "User ID is required"}), 400 info = assistant.get_chat_info(chat_id, user_id) return jsonify(info) except Exception as e: logger.error(f"Error getting chat info for {chat_id}: {e}") return jsonify({"error": "Could not fetch chat information"}), 500 @app.route('/chat//messages', methods=['GET']) def get_chat_messages(chat_id): """ Get all messages for a specific chat and user. Query parameters: - user_id: Required user identifier Response format: [ { "message_id": 1, "role": "user", "content": "Hello, I need help with farming", "timestamp": "2024-01-01T10:00:00", "imageUrl": "optional" }, { "message_id": 2, "role": "assistant", "content": "Hello! I'd be happy to help...", "timestamp": "2024-01-01T10:00:05" } ] """ if not assistant: return jsonify({"error": "Assistant not available"}), 503 try: if not chat_id: return jsonify({"error": "Chat ID is required"}), 400 user_id = request.args.get('user_id') if not user_id: return jsonify({"error": "User ID is required"}), 400 messages = assistant.get_messages(chat_id, user_id) return jsonify(messages) except Exception as e: logger.error(f"Error fetching messages for chat {chat_id}: {e}") return jsonify({"error": "Could not fetch chat messages"}), 500 @app.route('/chat/', methods=['DELETE']) def delete_chat(chat_id): """ Delete a specific chat and all its messages for a specific user. Query parameters: - user_id: Required user identifier Response format: { "status": "success", "message": "Chat deleted successfully", "chat_id": "chat_abc123" } """ if not assistant: return jsonify({"error": "Assistant not available"}), 503 try: if not chat_id: return jsonify({"error": "Chat ID is required"}), 400 user_id = request.args.get('user_id') if not user_id: return jsonify({"error": "User ID is required"}), 400 success = assistant.clear_history(chat_id, user_id) if success: return jsonify({ "status": "success", "message": f"Chat {chat_id} deleted successfully", "chat_id": chat_id }) else: return jsonify({ "status": "error", "message": f"Failed to delete chat {chat_id}", "chat_id": chat_id }), 500 except Exception as e: logger.error(f"Error deleting chat {chat_id}: {e}") return jsonify({"error": "Could not delete chat"}), 500 # --- New User Management Endpoints --- @app.route('/users//stats', methods=['GET']) def get_user_stats(user_id): """ Get statistics for a specific user. Response format: { "user_id": "user_abc123", "total_sessions": 5, "total_messages": 42, "recent_sessions_24h": 2, "average_messages_per_session": 8.4, "first_session": "2024-01-01T10:00:00", "last_activity": "2024-01-01T15:30:00" } """ if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: if not user_id: return jsonify({"error": "User ID is required"}), 400 stats = conv_manager.get_user_stats(user_id) return jsonify(stats) except Exception as e: logger.error(f"Error getting user stats for {user_id}: {e}") return jsonify({"error": "Could not fetch user statistics"}), 500 @app.route('/users//chats', methods=['DELETE']) def delete_all_user_chats(user_id): """ Delete all chats for a specific user. Response format: { "status": "success", "message": "All chats deleted successfully", "user_id": "user_abc123" } """ if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: if not user_id: return jsonify({"error": "User ID is required"}), 400 success = conv_manager.delete_all_user_data(user_id) if success: return jsonify({ "status": "success", "message": f"All chats deleted for user {user_id}", "user_id": user_id }) else: return jsonify({ "status": "error", "message": f"Failed to delete chats for user {user_id}", "user_id": user_id }), 500 except Exception as e: logger.error(f"Error deleting all chats for user {user_id}: {e}") return jsonify({"error": "Could not delete user chats"}), 500 @app.route('/users//export', methods=['GET']) def export_user_data(user_id): """ Export all data for a specific user. Response format: { "user_id": "user_abc123", "export_date": "2024-01-01T10:00:00", "stats": {...}, "sessions": [...] } """ if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: if not user_id: return jsonify({"error": "User ID is required"}), 400 # Get user stats and sessions stats = conv_manager.get_user_stats(user_id) sessions = conv_manager.get_all_chat_sessions(user_id) # Get all messages for each session detailed_sessions = [] for session in sessions: session_data = session.copy() messages = conv_manager.get_history(session['session_id'], user_id) session_data['messages'] = messages detailed_sessions.append(session_data) export_data = { "user_id": user_id, "export_date": datetime.utcnow().isoformat(), "stats": stats, "sessions": detailed_sessions, "total_exported_sessions": len(detailed_sessions), "total_exported_messages": sum(len(s.get('messages', [])) for s in detailed_sessions) } return jsonify(export_data) except Exception as e: logger.error(f"Error exporting data for user {user_id}: {e}") return jsonify({"error": "Could not export user data"}), 500 @app.route('/users/', methods=['DELETE']) def delete_user_account(user_id): """ Delete a user account and all associated data. Response format: { "status": "success", "message": "User account deleted successfully", "user_id": "user_abc123" } """ if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: if not user_id: return jsonify({"error": "User ID is required"}), 400 # Delete all user data success = conv_manager.delete_all_user_data(user_id) if success: return jsonify({ "status": "success", "message": f"User account {user_id} deleted successfully", "user_id": user_id }) else: return jsonify({ "status": "error", "message": f"Failed to delete user account {user_id}", "user_id": user_id }), 500 except Exception as e: logger.error(f"Error deleting user account {user_id}: {e}") return jsonify({"error": "Could not delete user account"}), 500 # --- Legacy Endpoints for Backward Compatibility --- @app.route('/history/sessions', methods=['GET']) def get_sessions(): """Legacy endpoint - redirects to /chats""" return get_all_chats() @app.route('/history/messages/', methods=['GET']) def get_messages(session_id): """Legacy endpoint - redirects to /chat//messages""" return get_chat_messages(session_id) @app.route('/clear', methods=['POST']) def clear_history(): """Legacy endpoint for clearing chat history""" if not assistant: return jsonify({"error": "Assistant is not available."}), 503 try: data = request.get_json() if not data: return jsonify({"error": "Request body is required"}), 400 session_id = data.get('session_id') if not session_id: return jsonify({"error": "Missing 'session_id' in request body"}), 400 success = assistant.clear_history(session_id) if success: return jsonify({ "status": "success", "message": f"History for session {session_id} was cleared." }) else: return jsonify({"error": "Failed to clear history."}), 500 except Exception as e: logger.error(f"Error in legacy clear endpoint: {e}") return jsonify({"error": f"Server error: {str(e)}"}), 500 # --- Health Check and Status Endpoints --- @app.route('/health', methods=['GET']) def health_check(): """Health check endpoint for monitoring""" try: # Check if core services are available services_status = { "assistant": assistant is not None, "conversation_manager": conv_manager is not None, "database": False } # Test database connectivity if conv_manager: try: # Try to get chat sessions (will test DB connection) conv_manager.get_all_chat_sessions() services_status["database"] = True except: services_status["database"] = False all_healthy = all(services_status.values()) return jsonify({ "status": "healthy" if all_healthy else "degraded", "services": services_status, "timestamp": conv_manager.manager._get_timestamp() if conv_manager else None }), 200 if all_healthy else 503 except Exception as e: logger.error(f"Health check failed: {e}") return jsonify({ "status": "unhealthy", "error": str(e) }), 503 @app.route('/stats', methods=['GET']) def get_stats(): """Get system statistics""" if not conv_manager: return jsonify({"error": "Conversation manager not available"}), 503 try: chats = assistant.get_all_chats() total_chats = len(chats) total_messages = sum(chat.get('message_count', 0) for chat in chats) # Recent activity (chats updated in last 24 hours) from datetime import datetime, timedelta now = datetime.utcnow() yesterday = now - timedelta(days=1) recent_chats = 0 for chat in chats: updated_at = chat.get('updated_at') if updated_at: try: chat_time = datetime.fromisoformat(updated_at.replace('Z', '+00:00')) if chat_time > yesterday: recent_chats += 1 except: pass return jsonify({ "total_chats": total_chats, "total_messages": total_messages, "recent_chats_24h": recent_chats, "average_messages_per_chat": round(total_messages / total_chats, 2) if total_chats > 0 else 0 }) except Exception as e: logger.error(f"Error getting stats: {e}") return jsonify({"error": "Could not fetch statistics"}), 500 # --- Error Handlers --- @app.errorhandler(404) def not_found(error): """Handle 404 errors""" return jsonify({"error": "Endpoint not found"}), 404 @app.errorhandler(500) def internal_error(error): """Handle 500 errors""" logger.error(f"Internal server error: {error}") return jsonify({"error": "Internal server error"}), 500 # --- Main Execution --- if __name__ == '__main__': # Set debug=False for production port = int(os.environ.get('PORT', 7860)) app.run(debug=False, port=port, host='0.0.0.0')