Create a database
Create a table
tenant_id
column? By specifying this column, You are making the table tenant aware. The rows in it will belong to specific tenants. If you leave it out, the table is considered shared, more on this later.
Get credentials
Get third party credentials
Set the environment
.env.example
to .env
and fill in the details of your Nile DB. The ones you copied and kept safe in step 3.It should look something like this:Run the application
Check the data in Nile
What's next?
main.py
is the entry point of the app. It sets up the FastAPI app, registers the middleware and has all the routes.
create_todo
method which is the route handler for @app.post("/api/todos")
. This handler executes when users add new tasks.
This is what the handler code looks like:
get_similar_tasks
, ai_estimate
and get_embedding
are all defined in ai_utils.py
.
They are wrappers around standard AI model calls and database queries, and they handle the specifics of the AI model we are using.
This will make it easy to switch models in the future.
Getting similar tasks is done by querying the database for tasks with similar embeddings. Before we search the database, we need to generate the embedding for the new task:
SEARCH_QUERY
task type. This is because we are looking for similar tasks to the new task. We use an embedding model
from the nomic
family, which is trained to perform specific types of embedding tasks. Telling it that we are generating the embedding for a lookup vs
generating an embedding that we will store with the document (as we’ll do in a bit), should help the model produce more relevant results.
In order to use vector embeddings with SQL Alchemy and SQL Model ORM, we used PG Vector’s Python library.
You’ll find it in requirements.txt
for the project. Note that we filter out results where the cosine distance is higher than 1.
The lower the cosine distance is, the more similar the tasks are (0 indicate that they are identical).
A cosine distance of 1 means that the vectors are essentially unrelated, and when cosine distance is closer to 2, it indicates that the vectors are semantically opposites.
The get_embedding
function uses the embedding model to generate the embedding and is a very simple wrapper on the model:
ai_estimate
to generate the time estimate.
This function also wraps a model, this time a conversation model rather than an embedding model. And it icludes the similar tasks in the promopt, so the model will
generate similar estimates:
TenantAwareMiddleware
is defined in middleware.py
,
and it is responsible for extracting the tenant ID from the request headers and setting it in the database session.
This ensures that all database operations are performed in the context of the current tenant.
login
or create_tenant
routes doesn’t need a tenant context.
Requests that don’t have a tenant context are considered to be global
since they are performed on the database as a whole, not in the virtual database for a specific tenant.
To handle a request in the global context, we use a global session. This is a session that doesn’t have a tenant context. For example to create a new tenant:
get_tenant_session
function sets the tenant context for the session, and the query is executed in the virtual database of the tenant.
The last piece of the puzzle is the get_tenant_session
function. It is defined in db.py
and is responsible for creating the session with the correct context.
yield
keyword is used to return the session to the caller, and the finally
block is used to clean up the session after the request is processed.
And this is it. Thats all we need to do to build a multi-tenant app with Nile, FastAPI and SQLAlchemy.