In this tutorial, we’ll show you how to build a complete Flask application that allows loading documents from Google Drive into a database, perform vector similarity search, and answer questions about the documents using OpenAI. By the end of this tutorial you’ll have a fully deployed application.

To follow this tutorial, you’ll need a Ploomber Cloud account, a Google Cloud account, and an OpenAI account.

Let’s get into it!

Download code

First, let’s install the ploomber-cloud CLI:

pip install ploomber-cloud

Download the sample code:

ploomber-cloud examples flask/gdrive-llm
cd gdrive-llm

And let’s initialize our project:

ploomber-cloud init

The init command will print our App ID, we’ll need this to configure our application on the Google Cloud console, so save it somewhere. For example, my init command printed:

Your app 'tight-limit-8371' has been configured successfully!

Hence, my App ID is tight-limit-8371.

The next sections explain the architecture and code in detail, if you rather skip to the deployment part, go to the Configure project section.

Architecture

architecture-diagram

Let’s explain the components of our system at a high-level.

The Flask app receives requests from our users and responds. It’ll ask users to authenticate and offer an interface for them to ask questions about their Google Drive documents.

The Database (SQLite) stores the documents downloaded from Google Drive and the embeddings (using OpenAI) we compute for each one. It also performs similarity search (via sqlite-vec) so we can retrieve the relevant documents when asking a question.

Since downloading all files, parsing them and computing embeddings takes some time. We have a job queue that allows us to execute this work in the background. The RabbitMQ server acts as a message broker, storing the background jobs in a queue, while the Celery worker processes these jobs by downloading documents from Google Drive, converting them to markdown, and computing embeddings that we’ll use later for similarity search.

Finally, Supervisor is a process control system that ensures our application components stay running. If any process crashes (like the Flask app or Celery worker), Supervisor automatically restarts it, making our application more reliable and resilient to failures.

Code walkthrough

The flask application (app.py)

The Flask app (app.py) handles all web requests and user interactions. The most important part is the authentication flow - when users visit /login, we use Google’s OAuth2 flow to authenticate them and get permission to access their Google Drive documents. The callback handler stores their credentials securely in the database:

@app.route("/oauth2callback")
def oauth2callback():
    # Get credentials from OAuth flow
    flow = Flow.from_client_config(CREDENTIALS, scopes=SCOPES,
                                 redirect_uri=url_for("oauth2callback", _external=True))
    flow.fetch_token(authorization_response=request.url)
    credentials = flow.credentials

    # Store credentials in database
    token_info = {
        "token": credentials.token,
        "refresh_token": credentials.refresh_token,
        # ... other token fields ...
    }
    with Session(engine) as db_session:
        user = db_session.query(User).filter_by(email=email).first()
        if user is None:
            # NOTE: in a production system we must use a secure secrets storage!
            user = User(email=email, token_info=json.dumps(token_info))
            db_session.add(user)

Once authenticated, users can trigger document loading via the /load endpoint, which starts a background Celery task to process their Google Drive documents. The /search endpoint handles question answering - it takes the user’s query, finds relevant documents using embedding similarity search, and generates an answer using GPT-4o mini:

@app.post("/search")
def search_post():
    query = request.form["query"]
    answer = answer_query(query, email=session["email"])
    return render_template("search-results.html", answer=answer)

The app uses Flask’s session management to keep track of logged-in users and implements a @login_required decorator to protect endpoints that require authentication. All database operations are handled through SQLAlchemy sessions, ensuring proper transaction management and connection handling.

Document loading

The document loading process is handled by the load_documents_from_user function in load.py. This function first retrieves the user’s stored Google credentials and uses them to initialize both Drive and Docs API clients. It then queries Google Drive for all Google Docs files (excluding trashed items):

query = "mimeType = 'application/vnd.google-apps.document' and trashed=false"
results = drive_service.files().list(
    q=query,
    pageSize=100,
    fields="nextPageToken, files(id, name)",
).execute()

For each document, we fetch its full content using the Docs API and convert it to markdown format using the convert_doc_to_markdown function. This function handles various document elements like headings, bold text, and italics, ensuring the document’s structure is preserved.

The most computationally intensive part is computing embeddings for all documents. We do this efficiently by batching all document contents together in a single API call to OpenAI’s embedding service. Finally, we store or update each document in the database with its content, embedding vector, and metadata:

embeddings = compute_embedding([md[2] for md in markdown_docs], return_single=False)
with Session(engine) as db_session:
    for (drive_id, name, content), embedding in zip(markdown_docs, embeddings):
        # Update existing or create new document
        db_doc = Document(
            name=name,
            content=content,
            embedding=embedding,
            google_drive_id=drive_id,
            user_id=user.id,
        )
        db_session.add(db_doc)

Answering questions

The question answering functionality is implemented in answer.py. When a user submits a query, we first compute an embedding vector for their question using OpenAI’s text-embedding-3-small model. This embedding is then used to find the most relevant documents from their Google Drive using similarity search.

The similarity search is implemented in the Document model using SQLite’s vector extensions. The find_similar method executes a SQL query that computes the L2 (Euclidean) distance between the query embedding and all document embeddings stored in the database, returning the closest matches:

@classmethod
def find_similar(cls, session, embedding, user_id, limit: int = None):
    query = text("""
        SELECT id, vec_distance_L2(embedding, :embedding) as distance
        FROM documents 
        WHERE user_id = :user_id
        ORDER BY distance
    """ + (f"LIMIT {limit}" if limit else ""))

Once we have the most relevant documents, we send them along with the user’s query to GPT-4o mini through OpenAI’s chat completions API. The documents are formatted as markdown and provided as context, allowing the model to generate an informed answer based on the user’s personal Google Drive content:

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant..."},
        {"role": "user", "content": "\n\n".join([doc.to_markdown() for doc in similar_docs])},
        {"role": "user", "content": query},
    ],
)

Keeping up with document updates

To keep documents up-to-date with changes in users' Google Drive, we use Celery for background task processing. The background.py module defines a periodic task that runs every 60 minutes to check for and process document updates for all users:

@app.on_after_configure.connect
def setup_periodic_tasks(sender: Celery, **kwargs):
    # 60 minutes
    PERIOD = 60 * 60
    
    sender.add_periodic_task(
        PERIOD,
        schedule_document_loading,
        name="schedule_document_loading",
    )

The scheduling process works in two stages. First, the schedule_document_loading task queries the database for all registered users. Then for each user, it asynchronously triggers the load_documents_from_user task which handles the actual document loading process. This task uses the stored OAuth credentials to access the user’s Google Drive, fetch any new or updated documents, and update the database accordingly:

@app.task
def schedule_document_loading():
    with Session(engine) as db_session:
        users = db_session.query(User).all()
        for user in users:
            load_documents_from_user.delay(
                email=user.email,
                limit=50,
            )

For simplicity, we’ve implemented a single periodic task that processes all users' documents at once. While this works for demonstration purposes, a production system would benefit from a more sophisticated scheduling approach. Instead of updating all users simultaneously, it would be better to distribute the load by scheduling individual tasks per user at different times throughout the day. This would prevent the task queue from getting overwhelmed and ensure more consistent performance. Additionally, tracking changes at the document level would allow for even finer-grained control, enabling the system to prioritize updates for frequently modified documents while reducing unnecessary processing of static content.

Configure project

Open the Google Cloud console and create a new project:

1-new-project

Once the process finishes, the notification bar will show up. Click on SELECT PROJECT:

2-select-project

Go to APIs & Services -> Library:

api-services-enabled-apis

Once the search bar appears, search for the Google Docs API:

search google docs api

And enable it:

google docs enable api

Go back and search Google Drive API, and enable it:

enable google drive api

Let’s now configure OAuth. Search auth in the top search bar and click on Google Auth Platform:

google auth platform

You should now see a screen like this, click on Get Started:

google auth platform get started

You’ll need to fill out a form with the details of your application. Note that Google has a policy about App name, so write a generic one (if your app name doesn’t comply with the policy, you can try again):

app info

In the Audience section, I recommend selecting External, otherwise, only members of your organization will be able to use the app.

app audience

Finally, enter a support email address:

contact info

Agree to the User Data Policy and click on CREATE:

finish app creation

Go back to Google Auth Platform and click on Branding. Scroll down and you’ll see an Authorized domains section, enter ploomber.app:

authorized domains

Now, click on Clients (left sidebar) -> + CREATE CLIENT. Select Web application as the Application type.

NOTE: If you encounter a message saying you have to configure the consent screen, ensure you properly configured the Authorized domains in the Branding section and wait for a minute or so.

web app client

Then, enter a name for your client and go to the Authorized redirect URIs and enter the following:

https://ID.ploomber.app/oauth2callback

Where ID is your Ploomber Cloud App ID. Scroll down and click on SAVE.

oauth client configuration

Once saved, you’ll be taken to the client list. Click on the arrow pointing downwards in the Actions section:

oauth client list

Once the client details are displayed, click on DOWNLOAD JSON:

download json oauth

Rename this file to credentials.json and store it in the parent directory of the example code you downloaded earlier.

Now, go to the Audience section and add some test users (NOTE: this is only required if you selected External in the audience when creating the app):

add test users

Our app is now fully configured on Google Cloud. Let’s now deploy it!

Deploy project

First, we need to ensure the app credentials and OpenAI API key are properly passed to our Flask app. Execute the following in your project’s root directory:

python -m gdrive_loader.env

This will prompt you for your OpenAI key and ensure that there is a credentials.json file (downloaded earlier) in the current directory.

We’re ready to deploy:

ploomber-cloud deploy

The command will print a URL to track progress. After a minute or so, the app will be available:

app ready

Once that happens, open the app URL and you’ll see this:

app home

Click on login to authenticate. You’ll see something like this (that’s expected since our app hasn’t been verified as we’re in test mode), click on Continue:

continue button

Next, enable all permissions:

permissions

NOTE: if you encounter an error like the one in the image above. Double check the Authorized redirect URIs, it must exactly match the URL from your app:

uri mismatch

Once you finish the login workflow, you’ll see the following:

logged in

Now, click on Load docs in the top right corner. This will start downloading documents from Google Drive. The process takes 1-2 minutes.

Once the process is done, you’ll see the documents in the Documents section. You can also track progress from the Ploomber Cloud platform, you’ll see something like this in the logs when processing documents:

2025-01-09T23:40:02.873000 - [2025-01-09 23:40:02,873: WARNING/ForkPoolWorker-2] Saved new document: DOC NAME

And something like this when all documents have been processed:

2025-01-09T23:40:02.883000 - [2025-01-09 23:40:02,883: INFO/ForkPoolWorker-2] Task gdrive_loader.background.load_documents_from_user[xyz] succeeded in 30.0s: None

Finally, click on Search and start asking questions about your documents!

Limitations

This application is a proof of concept designed to demonstrate the core functionality of integrating Google Drive with AI capabilities. As such, it has several limitations that make it unsuitable for production use. The application lacks important features like proper security measures (tokens aren’t encrypted), and robust error handling for Google Drive API calls. We’ve made some architectural choices to keep things simple, such as not storing complete documents (to stay within model context limits), using a single SQLite database (instead of separating user data), and implementing basic session management. Additionally, the lack of persistent storage means all data is wiped when the app restarts. If you’re interested in deploying a production-ready system that integrates Google Drive with AI capabilities, please reach out to us to discuss our enterprise-grade solutions that address these limitations.