Skip to content

Commit 815fe1f

Browse files
committed
refactor: improve gen-ai module architecture and separate logic]
1 parent 89f10da commit 815fe1f

File tree

10 files changed

+170
-93
lines changed

10 files changed

+170
-93
lines changed

src/di/di_modules.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
ChatMemory,
3+
DocumentsService,
4+
LLMService,
5+
} from '@/modules/genai/adapters'
6+
7+
import {
8+
SearchInDocumentUseCase,
9+
TranslateTextUseCase,
10+
} from '@/modules/genai/core'
11+
12+
const chatMemory = new ChatMemory()
13+
const llmService = new LLMService()
14+
const documentService = new DocumentsService(llmService)
15+
16+
const searchInDocumentUseCase = new SearchInDocumentUseCase(
17+
chatMemory,
18+
llmService,
19+
documentService,
20+
)
21+
22+
const translateUseCase = new TranslateTextUseCase()
23+
24+
export {
25+
chatMemory,
26+
documentService,
27+
llmService,
28+
searchInDocumentUseCase,
29+
translateUseCase
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { RedisChatMessageHistory } from '@langchain/redis'
2+
import { BufferMemory } from 'langchain/memory'
3+
4+
export class ChatMemory {
5+
constructor() {
6+
this._memory = new BufferMemory({
7+
chatHistory: new RedisChatMessageHistory({
8+
sessionId: 'a168c61a-c431-4ef8-bc1c-fedd808d45ea',
9+
sessionTTL: 3600,
10+
config: {
11+
url: process.env['REDIS_URL'],
12+
password: process.env['REDIS_PASSWORD'],
13+
},
14+
}),
15+
returnMessages: true,
16+
memoryKey: 'chat_history',
17+
})
18+
}
19+
20+
private _memory: BufferMemory
21+
22+
get memoryObject() {
23+
return this._memory
24+
}
25+
26+
async retrieveMemoryHistory(): Promise<Record<string, any>[]> {
27+
const memoryResults = await this._memory.loadMemoryVariables({})
28+
return memoryResults['chat_history']
29+
}
30+
31+
saveChatHistory(input: string, output: string): Promise<void> {
32+
return this._memory.saveContext({ input }, { output })
33+
}
34+
}

src/modules/genai/adapters/dataproviders/redis/redis-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export class RedisClient {
2121
await this._redisClient.quit()
2222
}
2323

24-
async storeValue(key: string, value: object): Promise<void> {
24+
async storeValue(key: string, value: object, ttl?: number): Promise<void> {
2525
await this._redisClient.set(key, JSON.stringify(value), {
26-
EX: 300,
26+
EX: ttl,
2727
NX: true,
2828
})
2929
}

src/modules/genai/adapters/inde.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './dataproviders/chat-memory/chat-memory'
2+
export * from './dataproviders/redis/redis-client'
3+
export * from './services/document/documents.service'
4+
export * from './services/llm/llm.service'
5+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
2+
import { Document } from 'langchain/document'
3+
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
4+
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
5+
import { LLMService } from '../llm/llm.service'
6+
7+
export class DocumentsService {
8+
constructor(private _llmService: LLMService) {}
9+
10+
convertDocsToString(documents: Document[]) {
11+
return documents
12+
.map((document) => `<doc>\n${document.pageContent}\n</doc>`)
13+
.join('\n')
14+
}
15+
16+
async initializeVectorStore(docs: Document<Record<string, any>>[]) {
17+
const splitter = new RecursiveCharacterTextSplitter({
18+
chunkSize: 1536,
19+
chunkOverlap: 128,
20+
})
21+
22+
const splitDocs = await splitter.splitDocuments(docs)
23+
const vectorstore = await MemoryVectorStore.fromDocuments(
24+
splitDocs,
25+
this._llmService.textEmbedding,
26+
)
27+
28+
const retriever = vectorstore.asRetriever()
29+
return { vectorstore, retriever }
30+
}
31+
32+
async loadDocument(filePath: string) {
33+
const systemPath = process.cwd()
34+
const loader = new PDFLoader(`${systemPath}${filePath}`, {
35+
parsedItemSeparator: '',
36+
})
37+
38+
return await loader.load()
39+
}
40+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
ChatGoogleGenerativeAI,
3+
GoogleGenerativeAIEmbeddings,
4+
} from '@langchain/google-genai'
5+
6+
export class LLMService {
7+
constructor() {}
8+
9+
get llm() {
10+
return new ChatGoogleGenerativeAI({
11+
model: 'gemini-1.5-flash',
12+
apiKey: process.env['GOOGLE_GENAI_API_KEY'],
13+
})
14+
}
15+
16+
get textEmbedding() {
17+
return new GoogleGenerativeAIEmbeddings({
18+
model: 'text-embedding-004',
19+
apiKey: process.env['GOOGLE_GENAI_API_KEY'],
20+
})
21+
}
22+
}

src/modules/genai/application/controllers/gen-ai.controller.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import { Request, Response } from 'express'
1+
import { Request, Response } from 'express';
22

3-
import {
4-
SearchInDocumentUseCase,
5-
TranslateTextUseCase,
6-
upload,
7-
} from '../../core'
3+
import { searchInDocumentUseCase, translateUseCase } from '@/di/di_modules';
4+
import { upload } from '../../core';
85

96
export class GenAIController {
107
async translateText(
118
req: Request<any, any, { text: string; language: string }>,
129
res: Response,
1310
) {
14-
// TODO: Implement DI
15-
const translateUseCase = new TranslateTextUseCase()
1611
const { text, language } = req.body
1712

1813
if (language === undefined || text === undefined) {
@@ -29,8 +24,6 @@ export class GenAIController {
2924
req: Request<any, any, { query: string }>,
3025
res: Response,
3126
) {
32-
const useCase = new SearchInDocumentUseCase()
33-
3427
upload(req, res, async (err) => {
3528
if (err) {
3629
return res
@@ -45,7 +38,10 @@ export class GenAIController {
4538
const query = req.body.query
4639
const filePath = `/uploads/${req.file.filename}`
4740

48-
const { result } = await useCase.invoke({ query, filePath })
41+
const { result } = await searchInDocumentUseCase.invoke({
42+
query,
43+
filePath,
44+
})
4945

5046
return res.json({
5147
message: 'File and query received successfully!',
Lines changed: 20 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
2-
import { Document } from '@langchain/core/documents'
31
import { StringOutputParser } from '@langchain/core/output_parsers'
42

53
import {
@@ -11,50 +9,34 @@ import {
119
RunnablePassthrough,
1210
RunnableSequence,
1311
} from '@langchain/core/runnables'
14-
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
15-
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
16-
17-
import {
18-
ChatGoogleGenerativeAI,
19-
GoogleGenerativeAIEmbeddings,
20-
} from '@langchain/google-genai'
21-
22-
import { UseCase } from '@/common/types'
23-
import { RedisChatMessageHistory } from '@langchain/redis'
24-
import { BufferMemory } from 'langchain/memory'
2512

2613
import {
2714
CONTEXTUALIZED_SYSTEM_PROMPT,
2815
SEARCH_DOC_SYSTEM_PROMPT,
2916
} from '../../utils'
3017

18+
import { UseCase } from '@/common/types'
3119
import { Params, Result } from './types'
3220

33-
/// TODO: delegate to RedisClient class
34-
const chatMemory = new BufferMemory({
35-
chatHistory: new RedisChatMessageHistory({
36-
sessionId: 'a168c61a-c431-4ef8-bc1c-fedd808d45ea',
37-
sessionTTL: 3600, // 300 = 5m, 3600 = 1h, null = never
38-
config: {
39-
url: process.env['REDIS_URL'],
40-
password: process.env['REDIS_PASSWORD'],
41-
},
42-
}),
43-
returnMessages: true,
44-
memoryKey: 'chat_history',
45-
})
21+
import {
22+
ChatMemory,
23+
DocumentsService,
24+
LLMService,
25+
} from '@/modules/genai/adapters'
4626

4727
export class SearchInDocumentUseCase implements UseCase<Result, Params> {
48-
async invoke({ filePath, query }: Params): Promise<Result> {
49-
const docs = await this._loadDocument(filePath)
50-
const vectorStore = await this._initializeVectorStoreWithDocuments(docs)
51-
const retriever = vectorStore.asRetriever()
52-
const llmModel = new ChatGoogleGenerativeAI({
53-
model: 'gemini-1.5-flash',
54-
apiKey: process.env['GOOGLE_GENAI_API_KEY'],
55-
})
28+
constructor(
29+
private _memory: ChatMemory,
30+
private _llmService: LLMService,
31+
private _documentService: DocumentsService,
32+
) {}
5633

57-
/// Standalone question referencing past context
34+
async invoke({ filePath, query }: Params): Promise<Result> {
35+
const llmModel = this._llmService.llm
36+
const docs = await this._documentService.loadDocument(filePath)
37+
const { retriever } = await this._documentService.initializeVectorStore(
38+
docs,
39+
)
5840

5941
const contextualizedPrompt = ChatPromptTemplate.fromMessages([
6042
['system', CONTEXTUALIZED_SYSTEM_PROMPT],
@@ -68,8 +50,6 @@ export class SearchInDocumentUseCase implements UseCase<Result, Params> {
6850
new StringOutputParser(),
6951
])
7052

71-
/// Standalone question end
72-
7353
const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([
7454
['system', SEARCH_DOC_SYSTEM_PROMPT],
7555
new MessagesPlaceholder('chat_history'),
@@ -83,7 +63,7 @@ export class SearchInDocumentUseCase implements UseCase<Result, Params> {
8363
return RunnableSequence.from([
8464
contextualizedQuestionChain,
8565
retriever,
86-
this._convertDocsToString,
66+
this._documentService.convertDocsToString,
8767
])
8868
}
8969

@@ -95,50 +75,13 @@ export class SearchInDocumentUseCase implements UseCase<Result, Params> {
9575
new StringOutputParser(),
9676
])
9777

98-
const memoryResults = await chatMemory.loadMemoryVariables({})
99-
const history = memoryResults['chat_history']
78+
const history = await this._memory.retrieveMemoryHistory()
10079
const result = await retrievalChain.invoke({
10180
question: query,
10281
chat_history: history,
10382
})
10483

105-
await chatMemory.saveContext({ input: query }, { output: result })
84+
this._memory.saveChatHistory(query, result)
10685
return { result }
10786
}
108-
109-
private async _loadDocument(filePath: string) {
110-
const systemPath = process.cwd()
111-
const loader = new PDFLoader(`${systemPath}${filePath}`, {
112-
parsedItemSeparator: '',
113-
})
114-
115-
return await loader.load()
116-
}
117-
118-
// TODO: improve it and delegate vector store to another layer
119-
private async _initializeVectorStoreWithDocuments(
120-
docs: Document<Record<string, any>>[],
121-
) {
122-
const splitter = new RecursiveCharacterTextSplitter({
123-
chunkSize: 1536,
124-
chunkOverlap: 128,
125-
})
126-
127-
const splitDocs = await splitter.splitDocuments(docs)
128-
const vectorstore = await MemoryVectorStore.fromDocuments(
129-
splitDocs,
130-
new GoogleGenerativeAIEmbeddings({
131-
apiKey: process.env['GOOGLE_GENAI_API_KEY'],
132-
model: 'text-embedding-004',
133-
}),
134-
)
135-
136-
return vectorstore
137-
}
138-
139-
private _convertDocsToString(documents: Document[]) {
140-
return documents
141-
.map((document) => `<doc>\n${document.pageContent}\n</doc>`)
142-
.join('\n')
143-
}
14487
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
export const SEARCH_DOC_SYSTEM_PROMPT = `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. Be verbose!
1+
export const SEARCH_DOC_SYSTEM_PROMPT = `You are an assistant for question-answering tasks.
2+
Rules:
3+
4+
- Use the following pieces of retrieved context to answer the question.
5+
- If you don't know the answer, just say that you don't know.
6+
- If the use ask something about a past conversation, you can check the chat history and analyze the content for answer the user properly.
27
38
<context>
49
{context}
510
</context>`
611

712
export const REPHRASE_QUESTION_SYSTEM_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.`
813

9-
export const CONTEXTUALIZED_SYSTEM_PROMPT = `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.`
14+
export const CONTEXTUALIZED_SYSTEM_PROMPT = `Given a chat history and the latest user question
15+
which might reference context in the chat history, formulate a standalone question
16+
which can be understood without the chat history. Do NOT answer the question,
17+
just reformulate it if needed and otherwise return it as is.`

0 commit comments

Comments
 (0)