Building a Gemini Clone with Python Flask and Firebase database & Auth

Building a Gemini Clone with Python Flask and Firebase database & Auth
2024-10-29Intermediate7 min

Are you interested in creating a real-time, Firebase-powered chat app using Python and Flask? This tutorial will guide you through building a Gemini clone with fully functional Firebase Authentication and Database capabilities, perfect for developers keen on crafting a responsive chat application. You’ll be able to check connectivity, manage chats, handle file inputs, and authenticate users with Firebase.

The code for this project is available on

GitHub

Prerequisites

Before starting, make sure you have the following:

  • Python installed
  • Flask library
  • Firebase account and project with Realtime Database and Authentication enabled
  • Google service account key JSON file for Firebase setup
  • Project Setup

    Step 1: Install Necessary Libraries

    To begin, install Flask and the Firebase Admin SDK with:

    pip install Flask firebase-admin

    Step 2: Initialize Firebase

    Create a serviceAccountKey.json file containing Firebase credentials. In main.py, we import this key to initialize the Firebase Admin SDK, which connects the app to your Firebase project.

    from firebase_admin import credentials, auth, db, storage
    
    cred = credentials.Certificate('./serviceAccountKey.json')
    firebase_admin.initialize_app(cred, {
        'databaseURL': 'https://your-database-url.firebaseio.com',
        'storageBucket': 'your-bucket-id.appspot.com'
    })

    Code Breakdown

    1. Home Page - User Authentication Check

    The home() route checks if a user is authenticated before allowing access to the home page. It retrieves the UID (User ID) from a cookie and verifies it with Firebase Authentication.

    @app.route("/")
    def home():
        uid = request.cookies.get('uid')
        if not uid:
            return redirect(url_for('login'))
        try:
            user = auth.get_user(uid)
        except Exception:
            return redirect(url_for('login'))
        # If verified, access user data in Firebase Realtime Database
        user_ref = db.reference(f'users/{uid}')
        snapshot = user_ref.get()
        if snapshot is None:
            user_ref.set({'createdAt': datetime.now().isoformat()})
        return render_template("index.html")

    2. Login Page

    This route renders the login.html page, where users can log in. The actual authentication process occurs on the client side using Firebase Authentication.

    @app.route("/login")
    def login():
        return render_template("login.html")

    3. Connection Check

    The /check_connection route validates that Firebase Database and Storage are accessible. It reads a dummy reference in the database and lists files in storage to confirm connectivity, returning a JSON response indicating the status.

    @app.route("/check_connection")
    def check_connection():
        response = {'database': 'Not connected', 'storage': 'Not connected'}
        try:
            ref = db.reference('/test_connection').get()
            response['database'] = 'Connected'
        except Exception as e:
            response['database'] = f'Failed to connect: {str(e)}'
        try:
            bucket = storage.bucket()
            blobs = bucket.list_blobs(max_results=1)
            response['storage'] = 'Connected'
        except Exception as e:
            response['storage'] = f'Failed to connect: {str(e)}'
        return jsonify(response)

    4. Create a New Chat

    The /createNewChat route lets users create a new chat by sending a POST request with chat text data. It checks for existing chats to avoid duplicates.

    @app.route('/createNewChat', methods=['POST'])
    def create_new_chat():
        data = request.get_json()
        text = data.get('text')
        uid = request.cookies.get('uid')
        ref = db.reference(f'users/{uid}/chats')
        if ref.child(text).get():
            return jsonify({'message': 'Chat already exists. Choose another one.'}), 400
        ref.child(text).set({'createdAt': datetime.now(timezone.utc).isoformat()})
        return jsonify({'message': 'Chat message added successfully.'}), 200

    5. Retrieve Chat List

    With the /getChats route, the user can fetch a list of existing chats. It retrieves chat names from Firebase and returns them in JSON format.

    @app.route('/getChats', methods=['GET'])
    def get_chats():
        uid = request.cookies.get('uid')
        chats_ref = db.reference(f'users/{uid}/chats')
        chat_names = list(chats_ref.get().keys())
        return jsonify({'chats': chat_names}), 200

    6. Chat with Gemini Model

    The /api route is a multi-purpose API endpoint that accepts user text input, image files, or audio files to simulate chat interactions with Gemini AI.

  • Text Input: Processed directly through the Gemini model.
  • Image Input: Secured, saved, and analyzed using the model.
  • Audio Input: Stored temporarily and passed to the model.
  • The function returns a response based on the input type, generating streaming responses from Gemini AI.

    @app.route("/api", methods=["POST"])
    def qa():
        if not os.path.exists('temp'):
            os.makedirs('temp')
        text = request.form.get('text', '')
        file = request.files.get('file')
        audioFile = request.files.get('audioFile')
        selectedChatName = request.form.get('selectedChatName')
        response_text = ""
        if file:
            filename = secure_filename(file.filename)
            file_path = os.path.join('temp', filename)
            file.save(file_path)
            # Process image and send to model
        elif audioFile:
            audio_filename = secure_filename(audioFile.filename)
            audio_file_path = os.path.join('temp', audio_filename)
            audioFile.save(audio_file_path)
            # Process audio and send to model
        else:
            prompt = f"{text}"
            response = chat.send_message(prompt, stream=True)
        def generate():
            for chunk in response:
                yield chunk.text
        return Response(generate(), content_type='text/plain')

    Firebase Auth with html

    Firebase Authentication for a smooth signup and login experience. The following HTML code (login.html) includes Firebase Auth configuration and functions to handle user sign-up and login, which helps manage authentication in our app seamlessly.

    In the beginning, the document specifies that it's an HTML file (<!DOCTYPE html>), with the default language set to English (lang="en"). The meta tags establish the document's character set as UTF-8 and set up the viewport for mobile responsiveness, enabling the page to adjust its layout on different screen sizes, including phones and tablets.

     <meta charset="UTF-8">
        <title>Sign up / Login Form</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="/static/css/login.css">

    We then link a CSS file located at /static/css/login.css, which contains the styles for the login and signup forms

    Firebase SDK Integration

    The Firebase Authentication SDKs are added next to power user authentication. Two SDKs are specifically included:

     <!-- Firebase SDKs -->
        <script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
        <script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
  • firebase-app-compat.js: Initializes the Firebase application, setting up the Firebase project in this environment.
  • firebase-auth-compat.js: Manages Firebase's Authentication services, enabling functions like creating new user accounts, logging in existing users, and handling user sessions.
  • These SDKs are hosted by Google and integrated using <script> tags. Each one is imported from Firebase’s own CDN, ensuring the latest compatible version is loaded without needing to manage versions locally.

    Firebase Configuration

    Following the SDK inclusion, we define the Firebase configuration in JavaScript. The firebaseConfig object contains the necessary details for initializing our Firebase app and connecting to the Firebase project

    <script>
        // Firebase configuration
        const firebaseConfig = {
            apiKey: "your-api-key",
            authDomain: "your-app-id.firebaseapp.com",
            projectId: "your-project-id",
            storageBucket: "your-storage-bucket",
            messagingSenderId: "your-messaging-sender-id",
            appId: "your-app-id",
            measurementId: "G-measurement-id"
        };
    
        // Initialize Firebase
        firebase.initializeApp(firebaseConfig);
        const auth = firebase.auth();
    </script>
  • apiKey: Authorizes requests from the app to Firebase services.
  • authDomain: Specifies the authentication domain, unique to our Firebase project.
  • projectId, storageBucket, messagingSenderId, and appId: All essential identifiers that point to this specific Firebase project instance.
  • measurementId: Used to connect Google Analytics if it’s enabled, allowing us to monitor user behavior and engagement.
  • This configuration is specific to our Firebase project and can be retrieved from the Firebase console. Using firebase.initializeApp(firebaseConfig), we initialize Firebase in our application, making the Firebase Authentication object (auth) available for authentication processes throughout the app.

    Sign-Up Function

    The signUp() function is where the user registration process takes place. It begins by capturing the user’s email and password from input fields with name="email" and name="pswd", respectively.

    <script>
        // Sign Up Function
        async function signUp(event) {
            event.preventDefault();
            const email = document.querySelector('input[name="email"]').value;
            const password = document.querySelector('input[name="pswd"]').value;
            try {
                const userCredential = await auth.createUserWithEmailAndPassword(email, password);
                const uid = userCredential.user.uid;
    
                // Set UID cookie for session persistence
                document.cookie = `uid=${uid}; max-age=${365 * 24 * 60 * 60}; path=/;`;
                window.location.href = '/'; // Redirect to home after sign-up
            } catch (error) {
                console.error('Error during sign-up:', error.message);
            }
        }
    </script>

    This function simplifies the sign-up process, delegating security checks and error handling to Firebase, which follows best practices in security for managing sensitive data like passwords.

    Login Function

    The login() function follows a similar pattern to signUp(), except that it’s geared toward existing users who are logging in.

    <script>
        // Login Function
        async function login(event) {
            event.preventDefault();
            const email = document.querySelector('input[name="loginemail"]').value;
            const password = document.querySelector('input[name="loginpswd"]').value;
            try {
                const userCredential = await auth.signInWithEmailAndPassword(email, password);
                const uid = userCredential.user.uid;
    
                // Set UID cookie for session persistence
                document.cookie = `uid=${uid}; max-age=${365 * 24 * 60 * 60}; path=/;`;
                window.location.href = '/'; // Redirect to home after login
            } catch (error) {
                console.error('Error during login:', error.message);
            }
        }
    </script>

    This function ensures that only registered users can log in, with Firebase handling password verification and user management, significantly reducing the need for our app to handle sensitive data directly.

    This login.html file encapsulates both the visual and functional aspects of user authentication for the Gemini Clone application. With Firebase managing the back-end logic for user authentication, this file focuses on securely handling user input, setting session cookies, and directing users to the correct location based on their actions. By leveraging Firebase, we eliminate the need for manual handling of user passwords, reducing risk and allowing Firebase’s secure infrastructure to handle user authentication.

    Main index.html file

    You will find this file on Github with full project code is available on

    GitHub

    Running the Application

    To start the server, run:

    python main.py

    You can access the app at http://localhost:5001

    This project has introduced you to building a full-stack Gemini clone using Python Flask and Firebase. You learned how to set up Firebase Authentication, handle file uploads, and communicate with a model like Gemini. With this knowledge, you can enhance the app further by adding features or optimizing performance. All the source code is on GitHub, so feel free to clone and explore!

    Happy coding!