DEV Community

Cover image for Building a Context-Aware To-Do List with Nestjs, RAG, Prisma, and Gemini API
es404020
es404020

Posted on

Building a Context-Aware To-Do List with Nestjs, RAG, Prisma, and Gemini API

In this tutorial, we will create a context-aware to-do list application using Retrieval-Augmented Generation (RAG). We'll leverage Google's Gemini API for text embeddings, PgVector for efficient vector storage, and Prisma with NestJS for managing the PostgreSQL database. This setup will allow advanced features like cleaning up duplicate tasks and retrieving contextually similar tasks.


Prerequisites

  1. Basic understanding of NestJS and Prisma.
  2. Node.js and npm installed.
  3. A PostgreSQL database with PgVector extension enabled.
  4. Access to Google Cloud with the Gemini API key.

Step 1: Set Up NestJS Project

  1. Create a new NestJS project:
   nest new todo-app
   cd todo-app
Enter fullscreen mode Exit fullscreen mode
  1. Remove unnecessary default files:
   rm src/app.controller.* src/app.service.* src/app.module.ts
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Dependencies

Install the required dependencies:

npm install prisma @prisma/client @google/generative-ai dotenv
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Prisma with PgVector

  1. Initialize Prisma:
   npx prisma init
Enter fullscreen mode Exit fullscreen mode
  1. Update the .env file with your PostgreSQL database credentials:
   DATABASE_URL="postgresql://<username>:<password>@localhost:5432/<database>?schema=public"
Enter fullscreen mode Exit fullscreen mode
  1. Enable PgVector in your schema.prisma file:
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["postgresqlExtensions"]
}

datasource db {
  provider   = "postgresql"
  url        = env("DATABASE_URL")
  extensions = [pgvector]
}

model Task {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  embedding Unsupported("vector(1536)")
}
Enter fullscreen mode Exit fullscreen mode
  1. Apply the database migration:
   npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Prisma in NestJS

Create a PrismaModule for database access:

// src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode

Import the PrismaModule in your main module:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [PrismaModule, TasksModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Step 5: Set Up the Tasks Module

  1. Generate the tasks module:
   nest generate module tasks
   nest generate service tasks
   nest generate controller tasks
Enter fullscreen mode Exit fullscreen mode
  1. Implement the TasksService:
   // src/tasks/tasks.service.ts
   import { Injectable } from '@nestjs/common';
   import { PrismaService } from '../prisma/prisma.service';
   import { Task } from '@prisma/client';
   import { GeminiService } from '../gemini/gemini.service';

   @Injectable()
   export class TasksService {
     constructor(private prisma: PrismaService, private geminiService: GeminiService) {}

     async createTask(title: string, content: string): Promise<Task> {
       const embedding = await this.geminiService.getEmbedding(`${title} ${content}`);
       return this.prisma.task.create({
         data: { title, content, embedding },
       });
     }

     async getTasks(): Promise<Task[]> {
       return this.prisma.task.findMany();
     }

     async findSimilarTasks(embedding: number[], limit = 5): Promise<any[]> {
       const embeddingStr = `[${embedding.join(',')}]`;
       return this.prisma.$queryRaw`
         SELECT *, embedding <-> ${embeddingStr}::vector AS distance
         FROM "Task"
         ORDER BY embedding <-> ${embeddingStr}::vector
         LIMIT ${limit};
       `;
     }
   }
Enter fullscreen mode Exit fullscreen mode
  1. Implement the TasksController:
   // src/tasks/tasks.controller.ts
   import { Controller, Post, Get, Body } from '@nestjs/common';
   import { TasksService } from './tasks.service';

   @Controller('tasks')
   export class TasksController {
     constructor(private tasksService: TasksService) {}

     @Post()
     async createTask(@Body('title') title: string, @Body('content') content: string) {
       return this.tasksService.createTask(title, content);
     }

     @Get()
     async getTasks() {
       return this.tasksService.getTasks();
     }
   }
Enter fullscreen mode Exit fullscreen mode

Step 6: Integrate Gemini API for Embedding Generation

  1. Create a GeminiService:
   // src/gemini/gemini.service.ts
   import { Injectable } from '@nestjs/common';
   import * as genai from '@google/generative-ai';

   @Injectable()
   export class GeminiService {
     private client: genai.GenerativeLanguageServiceClient;

     constructor() {
       this.client = new genai.GenerativeLanguageServiceClient({
         apiKey: process.env.GEMINI_API_KEY,
       });
     }

     async getEmbedding(text: string): Promise<number[]> {
       const result = await this.client.embedText({
         model: 'models/text-embedding-001',
         content: text,
       });
       return result.embedding;
     }
   }
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

With this setup, you have a fully functional to-do list application that can:

  1. Generate embeddings for task content using Gemini.
  2. Store embeddings in a PostgreSQL database using PgVector.
  3. Retrieve similar tasks based on their embeddings.

This architecture enables advanced features like semantic search and contextual data cleaning. Expand it further to build intelligent task management systems!

Top comments (0)