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 update it with the connection string you just copied from Nile Console and the configuration of your AI vendor and model.
Make sure you don’t include the word “psql”. It should look something like this:yarn install
or npm install
.Run the application
npm start
or yarn start
.
Now you can use curl
to explore the APIs. Here are a few examples:Check the data in Nile
What's next?
pg_vector
extension
and the vector data type, we had to manually add the vector column to the schema file:
tenantContext
, lets take a look at what is this context and how it is managed.
We also have queries that use the embedding
column with the vector
data type. This column is used to store embeddings for each task, which are used to generate automated time estimates for each task in the todo list.
Because Prisma does not yet support the vector
data type, we need to use Prisma’s executeRaw
and queryRaw
methods to work with this column.
We’ll show the example in the next section when we talk about the use of AI models to generate time estimates.
After this small detour, we’ll get back to the tenant context and how to use Prisma with virtual tenant databases.
app.post("/api/tenants/:tenantId/todos"
. This handler executes when users add new tasks.
This is what the handler code looks like:
findSimilarTasks
, aiEstimate
and embedTask
are all defined in AiUtils.ts
.
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.
Note that when we save the todo with the estimate and embedding, we use tenantDB.$queryRawUnsafe
to insert the data.
This is because Prisma does not yet support the vector
data type, so we need to go a bit lower level.
Getting similar tasks is done by querying the database for tasks with similar embeddings:
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.
We use queryRaw
to run the query because Prisma does not yet support the vector
data type, and we need to use both the
embedding column, and the <->
cosine distance operator in order to find the most similar tasks.
As you can see, 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 embedTask
function uses the embedding model to generate the embedding and is a very simple wrapper on the model:
aiEstimate
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:
app.post("/api/tenants/:tenantId/todos"
, this estimate is then stored in the database along with the task and its vector embedding.
AsyncLocalStorage
to store a Prisma Client with additional tenant information.
AsyncLocalStorage
is a Node.js feature that allows you to store data in a context that is local to the current execution flow.
tenantContext.run()
stores the Prisma Client in a local storage, accessible from anywhere in the current request execution flow. The second argument is a callback function that will be executed after the context is set.
Since this is an Express middleware, the callback is the next middleware or request handler in the chain.
The only last bit to understand is the initialization of the Prisma Client with tenantDB
.
AsyncLocalStorage
to store the Prisma Client in a context that is local to the current request execution flow.
The request handler then uses the Prisma Client from the context to run a query against the database. Since the tenant ID is already set in the database session, the query doesn’t need to filter the todos by tenant ID.