In this blog post, I describe how to use Langchain, NestJS, and Gemma 2 to build an RAG application about an Angular book in PDF format. Then, the HTMX and Handlebar template engine render the responses in a list. The application uses Langchain and its built-in PDF loader to load the PDF book and split the document into chunks. Then, Langchain uses the Gemini embedding text model to represent the documents into vectors and persist the vectors into a vector database. The vector store retriever provides the context for the large language model (LLM) to find information in its data to generate correct responses.
Set up environment variables
PORT=3001
GROQ_API_KEY=<GROQ API KEY>
GROQ_MODEL=gemma2-9b-it
GEMINI_API_KEY=<GEMINI API KEY>
GEMINI_TEXT_EMBEDDING_MODEL=text-embedding-004
HUGGINGFACE_API_KEY=<Huggingface API KEY>
HUGGINGFACE_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
QDRANT_URL=<Qdrant URL>
QDRANT_APK_KEY=<Qdrant API KEY>
Navigate to https://aistudio.google.com/app/apikey, sign in to create a new API Key. Replace the API Key to GENINI_API_KEY
.
Navigate to Groq Cloud, https://console.groq.com/, sign up and register a new API Key. Replace the API Key to GROQ_API_KEY
.
Navigate to Huggingface, https://huggingface.co/join, sign up and create a new access token. Replace the access token to HUGGINGFACE_API_KEY
.
Navigate to Qdrant, https://cloud.qdrant.io/, sign up and create a Qdrant space. Replace the URL to QDRANT_URL
. Replace the API Key to QDRANT_API_KEY
.
Install the dependencies
npm i -save-exact @google/generative-ai @huggingface/inference
@langchain/community @langchain/core @langchain/google-genai
@langchain/groq @langchain/qdrant @nestjs/config @nestjs/swagger
@nestjs/throttler class-transformer class-validator compression hbs
langchain pdf-parse
npm i -save-exact –save-dev @commitlint/cli
@commitlint/config-conventional
husky lint-staged
Define the configuration in the application
Create a src/configs folder and add a configuration.ts to it
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
groq: {
apiKey: process.env.GROQ_API_KEY || '',
model: process.env.GROQ_MODEL || 'gemma2-9b-it',
},
gemini: {
apiKey: process.env.GEMINI_API_KEY || '',
embeddingModel: process.env.GEMINI_TEXT_EMBEDDING_MODEL || 'text-embedding-004',
},
huggingface: {
apiKey: process.env.HUGGINGFACE_API_KEY || '',
embeddingModel: process.env.HUGGINGFACE_EMBEDDING_MODEL || 'BAAI/bge-small-en-v1.5',
},
qdrant: {
url: process.env.QDRANT_URL || 'http://localhost:6333',
apiKey: process.env.QDRANT_APK_KEY || '',
},
});
Create a Groq Module
Generate a Groq module, a controller and a service.
nest g mo groq
nest g s groq/application/groq --flat
nest g co groq/presenters/http/groq --flat
Add a chat model
Define a Groq configuration type, application/types/groq-config.type.ts
, in the module. The configuration service converts the configuration values to a custom Object.
export type GroqConfig = {
model: string;
apiKey: string;
};
Add a custom provider to provide an instance of GroqChatModel
. Create a groq.constant.ts
file under application/constants
folder.
// application/constants/groq.constant.ts
export const GROQ_CHAT_MODEL = 'GROQ_CHAT_MODEL';
// application/providers/groq-chat-model.provider.ts
import { ChatGroq } from '@langchain/groq';
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GROQ_CHAT_MODEL } from '~groq/application/constants/groq.constant';
import { GroqConfig } from '~groq/application/types/groq-config.type';
export const GroqChatModelProvider: Provider<ChatGroq> = {
provide: GROQ_CHAT_MODEL,
useFactory: (configService: ConfigService) => {
const { apiKey, model } = configService.get<GroqConfig>('groq');
return new ChatGroq({
apiKey,
model,
temperature: 0.1,
maxTokens: 2048,
streaming: false,
});
},
inject: [ConfigService],
};
Test the Groq Chat Model in a controller
import { MessageContent } from '@langchain/core/messages';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { ChatGroq } from '@langchain/groq';
import { Inject, Injectable } from '@nestjs/common';
import { GROQ_CHAT_MODEL } from './constants/groq.constant';
@Injectable()
export class GroqService {
constructor(@Inject(GROQ_CHAT_MODEL) private model: ChatGroq) {}
async generateText(input: string): Promise<MessageContent> {
const prompt = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful assistant'],
['human', '{input}'],
]);
const chain = prompt.pipe(this.model);
const response = await chain.invoke({
input,
});
return response.content;
}
}
The GroqService
service has a method to take a query and ask the model to generate a text response
@Controller('groq')
export class GroqController {
constructor(private service: GroqService) {}
@Get()
testChain(): Promise<MessageContent> {
return this.service.generateText('What is Agentic RAG?');
}
}
Export the chat model from the module
import { Module } from '@nestjs/common';
import { GroqChatModelProvider } from './application/providers/groq-chat-model.provider';
import { GroqService } from './application/groq.service';
import { GroqController } from './presenters/http/groq.controller';
@Module({
providers: [GroqChatModelProvider, GroqService],
controllers: [GroqController],
exports: [GroqChatModelProvider],
})
export class GroqModule {}
Create a vector store module
nest g mo vectorStore
nest g s application/vectorStore --flat
Add configuration types
Define configuration types under application/types
folder.
This is the configuration type of embedding model. This application supports both Gemini Text Embedding model and Huggingface Inference Embedding Model.
// application/types/embedding-model-config.type.ts
export type EmbeddingModelConfig = {
apiKey: string;
embeddingModel: string;
};
The application supports memory vector store and Qdrant vector store. Therefore, the application has Qdrant configuration.
// application/types/qdrant-database-config.type.ts
export type QdrantDatabaseConfig = {
apiKey: string;
url: string;
};
This configuration stores the split documents, vector database type and embedding model.
export type VectorDatabasesType = 'MEMORY' | 'QDRANT';
// application/types/vector-store-config.type.ts
import { Document } from '@langchain/core/documents';
import { Embeddings } from '@langchain/core/embeddings';
import { VectorDatabasesType } from './vector-databases.type';
export type VectorDatabaseFactoryConfig = {
docs: Document<Record<string, any>>[];
type: VectorDatabasesType;
embeddings: Embeddings;
};
export type DatabaseConfig = Omit<VectorDatabaseFactoryConfig, 'type'>;
Create a configurable embedding model
export type EmbeddingModels = 'GEMINI_AI' | 'HUGGINGFACE_INFERENCE';
import { TaskType } from '@google/generative-ai';
import { HuggingFaceInferenceEmbeddings } from '@langchain/community/embeddings/hf';
import { Embeddings } from '@langchain/core/embeddings';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EmbeddingModelConfig } from '../types/embedding-model-config.type';
import { EmbeddingModels } from '../types/embedding-models.type';
function createGeminiTextEmbeddingModel(configService: ConfigService) {
const { apiKey, embeddingModel: model } = configService.get<EmbeddingModelConfig>('gemini');
return new GoogleGenerativeAIEmbeddings({
apiKey,
model,
taskType: TaskType.RETRIEVAL_DOCUMENT,
title: 'Angular Book',
});
}
function createHuggingfaceInferenceEmbeddingModel(configService: ConfigService) {
const { apiKey, embeddingModel: model } = configService.get<EmbeddingModelConfig>('huggingface');
return new HuggingFaceInferenceEmbeddings({
apiKey,
model,
});
}
export function createTextEmbeddingModel(configService: ConfigService, embeddingModel: EmbeddingModels): Embeddings {
if (embeddingModel === 'GEMINI_AI') {
return createGeminiTextEmbeddingModel(configService);
} else if (embeddingModel === 'HUGGINGFACE_INFERENCE') {
return createHuggingfaceInferenceEmbeddingModel(configService);
} else {
throw new InternalServerErrorException('Invalid type of embedding model.');
}
}
The createGeminiTextEmbeddingModel
function instantiates and returns a Gemini text embedding model. Similarly, the createHuggingfaceInferenceEmbeddingModel
instantiates and returns a huggingface inference embedding model. Finally, the createTextEmbeddingModel
function is a factory method that creates the embedding model based on the embedding model flag.
Create a configurable vector store retriever
Define a contract of a Vector Database Service
// application/interfaces/vector-database.interface.ts
import { VectorStore, VectorStoreRetriever } from '@langchain/core/vectorstores';
import { DatabaseConfig } from '../types/vector-store-config.type';
export interface VectorDatabase {
init(config: DatabaseConfig): Promise<void>;
asRetriever(): VectorStoreRetriever<VectorStore>;
}
import { VectorStore, VectorStoreRetriever } from '@langchain/core/vectorstores';
import { Injectable, Logger } from '@nestjs/common';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { VectorDatabase } from '../interfaces/vector-database.interface';
import { DatabaseConfig } from '../types/vector-store-config.type';
@Injectable()
export class MemoryVectorDBService implements VectorDatabase {
private readonly logger = new Logger(MemoryVectorDBService.name);
private vectorStore: VectorStore;
async init({ docs, embeddings }: DatabaseConfig): Promise<void> {
this.logger.log('MemoryVectorStoreService init called');
this.vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);
}
asRetriever(): VectorStoreRetriever<VectorStore> {
return this.vectorStore.asRetriever();
}
}
The MemoryVectorDBService
implements the interface, persists the vectors into the memory store, and returns the vector store retriever.
import { VectorStore, VectorStoreRetriever } from '@langchain/core/vectorstores';
import { QdrantVectorStore } from '@langchain/qdrant';
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { QdrantClient } from '@qdrant/js-client-rest';
import { VectorDatabase } from '../interfaces/vector-database.interface';
import { QdrantDatabaseConfig } from '../types/qdrant-database-config.type';
import { DatabaseConfig } from '../types/vector-store-config.type';
const COLLECTION_NAME = 'angular_evolution_collection';
@Injectable()
export class QdrantVectorDBService implements VectorDatabase {
private readonly logger = new Logger(QdrantVectorDBService.name);
private vectorStore: VectorStore;
constructor(private configService: ConfigService) {}
async init({ docs, embeddings }: DatabaseConfig): Promise<void> {
this.logger.log('QdrantVectorStoreService init called');
const { url, apiKey } = this.configService.get<QdrantDatabaseConfig>('qdrant');
const client = new QdrantClient({ url, apiKey });
const { exists: isCollectionExists } = await client.collectionExists(COLLECTION_NAME);
if (isCollectionExists) {
const isDeleted = await client.deleteCollection(COLLECTION_NAME);
if (!isDeleted) {
throw new InternalServerErrorException(`Unable to delete ${COLLECTION_NAME}`);
}
this.logger.log(`QdrantVectorStoreService deletes ${COLLECTION_NAME}. Result -> ${isDeleted}`);
}
const size = (await embeddings.embedQuery('test')).length;
const isSuccess = await client.createCollection(COLLECTION_NAME, {
vectors: { size, distance: 'Cosine' },
});
if (!isSuccess) {
throw new InternalServerErrorException(`Unable to create collection ${COLLECTION_NAME}`);
}
this.vectorStore = await QdrantVectorStore.fromDocuments(docs, embeddings, {
client,
collectionName: COLLECTION_NAME,
});
}
asRetriever(): VectorStoreRetriever<VectorStore> {
return this.vectorStore.asRetriever();
}
}
The QdrantVectorDBService
implements the interface, persists the vectors into the Qdrant vector database, and returns the vector store retriever.
// application/vector-databases/create-vector-database.t
import { InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { VectorDatabasesType } from '../types/vector-databases.type';
import { MemoryVectorDBService } from './memory-vector-db.service';
import { QdrantVectorDBService } from './qdrant-vector-db.service';
export function createVectorDatabase(type: VectorDatabasesType, configService: ConfigService) {
if (type === 'MEMORY') {
return new MemoryVectorDBService();
} else if (type === 'QDRANT') {
return new QdrantVectorDBService(configService);
}
throw new InternalServerErrorException(`Invalid vector store type: ${type}`);
}
The function instantiates the database service based on the database type.
Create document chunks from an Angular PDF book
Copy the book to the assets folder
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 100,
});
export async function loadPdf(path: string) {
const loader = new PDFLoader(path);
const docs = await loader.load();
const splitDocs = await splitter.splitDocuments(docs);
return splitDocs;
}
The loadPdf
function uses the pdf loader to load the PDF file, and split the document into many chunks.
import { Embeddings } from '@langchain/core/embeddings';
import { VectorStore, VectorStoreRetriever } from '@langchain/core/vectorstores';
import { Inject, Injectable, Logger } from '@nestjs/common';
import path from 'path';
import { appConfig } from '~configs/root-path.config';
import { ANGULAR_EVOLUTION_BOOK, TEXT_EMBEDDING_MODEL, VECTOR_DATABASE } from './constants/rag.constant';
import { VectorDatabase } from './interfaces/vector-database.interface';
import { loadPdf } from './loaders/pdf-loader';
@Injectable()
export class VectorStoreService {
private readonly logger = new Logger(VectorStoreService.name);
constructor(
@Inject(TEXT_EMBEDDING_MODEL) embeddings: Embeddings,
@Inject(VECTOR_DATABASE) private dbService: VectorDatabase,
) {
this.createDatabase(embeddings, this.dbService);
}
private async createDatabase(embeddings: Embeddings, dbService: VectorDatabase) {
const docs = await this.loadDocuments();
await dbService.init({ docs, embeddings });
}
private async loadDocuments() {
const bookFullPath = path.join(appConfig.rootPath, ANGULAR_EVOLUTION_BOOK);
const docs = await loadPdf(bookFullPath);
this.logger.log(`number of docs -> ${docs.length}`);
return docs;
}
asRetriever(): VectorStoreRetriever<VectorStore> {
this.logger.log(`return vector retriever`);
return this.dbService.asRetriever();
}
}
The VectorStoreService
stores the PDF book into the vector database, and returns the vector store retriever.
Make the module a dynamic module
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TEXT_EMBEDDING_MODEL, VECTOR_DATABASE, VECTOR_STORE_TYPE } from './application/constants/rag.constant';
import { createTextEmbeddingModel } from './application/embeddings/create-embedding-model';
import { EmbeddingModels } from './application/types/embedding-models.type';
import { VectorDatabasesType } from './application/types/vector-databases.type';
import { createVectorDatabase, MemoryVectorDBService, QdrantVectorDBService } from './application/vector-databases';
import { VectorStoreTestService } from './application/vector-store-test.service';
import { VectorStoreService } from './application/vector-store.service';
import { VectorStoreController } from './presenters/http/vector-store.controller';
@Module({
providers: [VectorStoreService, VectorStoreTestService, MemoryVectorDBService, QdrantVectorDBService],
controllers: [VectorStoreController],
exports: [VectorStoreService],
})
export class VectorStoreModule {
static register(embeddingModel: EmbeddingModels, vectorStoreType: VectorDatabasesType): DynamicModule {
return {
module: VectorStoreModule,
providers: [
{
provide: TEXT_EMBEDDING_MODEL,
useFactory: (configService: ConfigService) => createTextEmbeddingModel(configService, embeddingModel),
inject: [ConfigService],
},
{
provide: VECTOR_STORE_TYPE,
useValue: vectorStoreType,
},
{
provide: VECTOR_DATABASE,
useFactory: (type: VectorDatabasesType, configService: ConfigService) =>
createVectorDatabase(type, configService),
inject: [VECTOR_STORE_TYPE, ConfigService],
},
],
};
}
}
The VectorStoreModule
is a dynamic module; the embedding model and vector database are configurable. The register static method create text embedding module and vector database based on the configuration.
Create an RAG module
The rag module is responsible for creating a langchain chain that asks the model to generate responses.
nest g mo ragTechBook
nest g s ragTechBook/application/rag --flat
nest g s ragTechBook/presenters/http/rag --flat
Create the RAG Service
// application/constants/prompts.constant.ts
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
const qaSystemPrompt = `You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know.
{context}`;
export const qaPrompt = ChatPromptTemplate.fromMessages([
['system', qaSystemPrompt],
new MessagesPlaceholder('chat_history'),
['human', '{question}'],
]);
const contextualizeQSystemPrompt = `Given a chat history and the latest user question
which might reference context in the chat history, formulate a standalone question
which can be understood without the chat history. Do NOT answer the question,
just reformulate it if needed and otherwise return it as is.`;
export const contextualizeQPrompt = ChatPromptTemplate.fromMessages([
['system', contextualizeQSystemPrompt],
new MessagesPlaceholder('chat_history'),
['human', '{question}'],
]);
This constant file stores some prompts for the Langchain chains.
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatGroq } from '@langchain/groq';
import { contextualizeQPrompt } from '../constants/prompts.constant';
export function createContextualizedQuestion(llm: ChatGroq) {
const contextualizeQChain = contextualizeQPrompt.pipe(llm).pipe(new StringOutputParser());
return (input: Record<string, unknown>) => {
if ('chat_history' in input) {
return contextualizeQChain;
}
return input.question;
};
}
This function creates a chain that formulates a question without relying on the chat history.
import { BaseMessage } from '@langchain/core/messages';
import { Runnable, RunnablePassthrough, RunnableSequence } from '@langchain/core/runnables';
import { ChatGroq } from '@langchain/groq';
import { Inject, Injectable } from '@nestjs/common';
import { formatDocumentsAsString } from 'langchain/util/document';
import { GROQ_CHAT_MODEL } from '~groq/application/constants/groq.constant';
import { VectorStoreService } from '~vector-store/application/vector-store.service';
import { createContextualizedQuestion } from './chain-with-history/create-contextual-chain';
import { qaPrompt } from './constants/prompts.constant';
import { ConversationContent } from './types/conversation-content.type';
@Injectable()
export class RagService {
private chat_history: BaseMessage[] = [];
constructor(
@Inject(GROQ_CHAT_MODEL) private model: ChatGroq,
private vectorStoreService: VectorStoreService,
) {}
async ask(question: string): Promise<ConversationContent[]> {
const contextualizedQuestion = createContextualizedQuestion(this.model);
const retriever = this.vectorStoreService.asRetriever();
try {
const ragChain = RunnableSequence.from([
RunnablePassthrough.assign({
context: (input: Record<string, unknown>) => {
if ('chat_history' in input) {
const chain = contextualizedQuestion(input);
return (chain as Runnable).pipe(retriever).pipe(formatDocumentsAsString);
}
return '';
},
}),
qaPrompt,
this.model,
]);
const aiMessage = await ragChain.invoke({ question, chat_history: this.chat_history });
this.chat_history = this.chat_history.concat(aiMessage);
if (this.chat_history.length > 10) {
this.chat_history.shift();
}
return [
{
role: 'Human',
content: question,
},
{
role: 'Assistant',
content: (aiMessage.content as string) || '',
},
];
} catch (ex) {
console.error(ex);
throw ex;
}
}
}
The RagService
service is straightforward. The ask
method submits the input to the chain and outputs a response. This method extracts the content from the response, stores the Human and AI messages in chat history in memory and returns the conversation to the template engine for rendering.
Add RAG Controller
import { IsNotEmpty, IsString } from 'class-validator';
export class AskDto {
@IsString()
@IsNotEmpty()
query: string;
}
@Controller('rag')
export class RagController {
constructor(private service: RagService) {}
@Post()
async ask(@Body() dto: AskDto): Promise<string> {
const conversation = await this.service.ask(dto.query);
return toDivRow(conversation);
}
}
The RAG controller submits the query to the chain, gets the results, and sends the HTML codes back to the template engine to render.
Import modules into RAG module
import { Module } from '@nestjs/common';
import { GroqModule } from '~groq/groq.module';
import { VectorStoreModule } from '~vector-store/vector-store.module';
import { RagService } from './application/rag.service';
import { RagController } from './presenters/http/rag.controller';
@Module({
imports: [GroqModule, VectorStoreModule.register('GEMINI_AI', 'MEMORY')],
providers: [RagService],
controllers: [RagController],
})
export class RagTechBookModule {}
Import RagModule to AppModule
import { RagTechBookModule } from '~rag-tech-book/rag-tech-book.module';
@Module({
imports: [
… other imports …
RagTechBookModule,
],
controllers: [AppController],
})
export class AppModule {}
Modify App Controller to render handlebar template
import { Controller, Get, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index')
getHello(): Record<string, string> {
return {
title: 'Angular Tech Book RAG',
};
}
}
The App controller informs the Handlebar template engine to render index.hbs
file.
HTMX and Handlebar Template Engine
This is a simple user interface to display the conversation
default.hbs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="Angular tech book RAG powed by gemma 2 LLM." />
<meta name="author" content="Connie Leung" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{{ title }}}</title>
<style>
*, *::before, *::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
</head>
<body class="p-4 w-screen h-screen min-h-full">
<script src="https://unpkg.com/htmx.org@2.0.1" integrity="sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/" crossorigin="anonymous"></script>
<div class="h-full grid grid-rows-[auto_1fr_40px] grid-cols-[1fr]">
{{> header }}
{{{ body }}}
{{> footer }}
</div>
</body>
</html>
The above is a default layout with a header, footer, and body. The body eventually displays the conversation between the AI and human. The head section imports tailwind to style the HTML elements and htmx to interact with the server.
<div>
<div class="mb-2 p-1 border border-solid border-[#464646] rounded-lg">
<p class="text-[1.25rem] mb-2 text-[#464646] underline">Architecture</p>
<ul>
<li class="text-[1rem]">Chat Model: Groq</li>
<li class="text-[1rem]">LLM: Gemma 2</li>
<li class="text-[1rem]">Embeddings: Gemini AI Embedding / HuggingFace Embedding</li>
<li class="text-[1rem]">Vector Store: Memory Vector Store / Qdrant Vector Store</li>
<li class="text-[1rem]">Retriever: Vector Store Retriever</li>
</ul>
</div>
<div id="chat-list" class="mb-4 h-[300px] overflow-y-auto overflow-x-auto">
<div class="flex text-[#464646] text-[1.25rem] italic underline">
<span class="w-1/5 p-1 border border-solid border-[#464646]">Role</span>
<span class="w-4/5 p-1 border border-solid border-[#464646]">Result</span>
</div>
</div>
<form id="rag-form" hx-post="/rag" hx-target="#chat-list" hx-swap="beforeend swap:1s">
<div>
<label>
<span class="text-[1rem] mr-1 w-1/5 mb-2 text-[#464646]">Question: </span>
<input type="text" name="query" class="mb-4 w-4/5 rounded-md p-2"
placeholder="Ask me something"
aria-placeholder="Placeholder to ask question to RAG"></input>
</label>
</div>
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white p-2 text-[1rem] flex justify-center items-center rounded-lg">
<span class="mr-1">Send</span><img class="w-4 h-4 htmx-indicator" src="/images/spinner.gif">
</button>
</form>
</div>
A user can input the question in the text field and click the Send button. The button makes a POST request to /rag
and appends the conversation to the list.
This is the end of my first langchain RAG application using the Gemma 2 model to generate the responses.
Top comments (3)
Thank you for your time in creating this article. It helps me learn how to use RAG in Angular.
Thanks. This application does not use Angular. It uses HTMX to build simple user interfaces. I use the technology to learn Angular from a RAG application that consumes an Angular book
Nice detailed tutorial on RAG tech, I read it from Google Dev Wechat and come back here for more understanding😂.