138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import { ChatAnthropic } from '@langchain/anthropic'
|
|
import { HumanMessage, SystemMessage } from '@langchain/core/messages'
|
|
import { CHATBOT_CONFIG } from './config'
|
|
import {
|
|
SYSTEM_PROMPT,
|
|
formatContextFromSources,
|
|
formatConversationHistory,
|
|
} from './prompts'
|
|
import {
|
|
retrieveRelevantDocuments,
|
|
documentsToSources,
|
|
type RetrievedDocument,
|
|
} from './retriever'
|
|
import type { ChatMessage, SourceReference } from '@/types/chat'
|
|
|
|
// Initialize the LLM
|
|
function getChatModel() {
|
|
return new ChatAnthropic({
|
|
modelName: CHATBOT_CONFIG.model,
|
|
maxTokens: CHATBOT_CONFIG.maxTokens,
|
|
temperature: CHATBOT_CONFIG.temperature,
|
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
})
|
|
}
|
|
|
|
export interface ChatResult {
|
|
content: string
|
|
sources: SourceReference[]
|
|
}
|
|
|
|
export async function generateChatResponse(
|
|
userMessage: string,
|
|
conversationHistory: ChatMessage[]
|
|
): Promise<ChatResult> {
|
|
// 1. Retrieve relevant documents
|
|
const relevantDocs = await retrieveRelevantDocuments(userMessage)
|
|
|
|
// 2. Format context from retrieved documents
|
|
const context = formatContextFromSources(
|
|
relevantDocs.map((doc) => ({
|
|
content: doc.content,
|
|
title: doc.title,
|
|
section_title: doc.section_title,
|
|
source_file: doc.source_file,
|
|
}))
|
|
)
|
|
|
|
// 3. Format conversation history (last N messages)
|
|
const recentHistory = conversationHistory.slice(
|
|
-CHATBOT_CONFIG.maxHistoryMessages
|
|
)
|
|
const historyText = formatConversationHistory(
|
|
recentHistory.map((msg) => ({
|
|
role: msg.role,
|
|
content: msg.content,
|
|
}))
|
|
)
|
|
|
|
// 4. Build the system prompt with context
|
|
const systemPrompt = SYSTEM_PROMPT.replace('{context}', context).replace(
|
|
'{history}',
|
|
historyText
|
|
)
|
|
|
|
// 5. Generate response
|
|
const model = getChatModel()
|
|
const response = await model.invoke([
|
|
new SystemMessage(systemPrompt),
|
|
new HumanMessage(userMessage),
|
|
])
|
|
|
|
// 6. Extract content and sources
|
|
const content =
|
|
typeof response.content === 'string'
|
|
? response.content
|
|
: JSON.stringify(response.content)
|
|
|
|
return {
|
|
content,
|
|
sources: documentsToSources(relevantDocs),
|
|
}
|
|
}
|
|
|
|
export async function* streamChatResponse(
|
|
userMessage: string,
|
|
conversationHistory: ChatMessage[]
|
|
): AsyncGenerator<{ type: 'content' | 'sources'; data: string | SourceReference[] }> {
|
|
// 1. Retrieve relevant documents first
|
|
const relevantDocs = await retrieveRelevantDocuments(userMessage)
|
|
|
|
// 2. Format context from retrieved documents
|
|
const context = formatContextFromSources(
|
|
relevantDocs.map((doc) => ({
|
|
content: doc.content,
|
|
title: doc.title,
|
|
section_title: doc.section_title,
|
|
source_file: doc.source_file,
|
|
}))
|
|
)
|
|
|
|
// 3. Format conversation history
|
|
const recentHistory = conversationHistory.slice(
|
|
-CHATBOT_CONFIG.maxHistoryMessages
|
|
)
|
|
const historyText = formatConversationHistory(
|
|
recentHistory.map((msg) => ({
|
|
role: msg.role,
|
|
content: msg.content,
|
|
}))
|
|
)
|
|
|
|
// 4. Build the system prompt
|
|
const systemPrompt = SYSTEM_PROMPT.replace('{context}', context).replace(
|
|
'{history}',
|
|
historyText
|
|
)
|
|
|
|
// 5. Stream the response
|
|
const model = getChatModel()
|
|
const stream = await model.stream([
|
|
new SystemMessage(systemPrompt),
|
|
new HumanMessage(userMessage),
|
|
])
|
|
|
|
for await (const chunk of stream) {
|
|
const content =
|
|
typeof chunk.content === 'string'
|
|
? chunk.content
|
|
: JSON.stringify(chunk.content)
|
|
if (content) {
|
|
yield { type: 'content', data: content }
|
|
}
|
|
}
|
|
|
|
// 6. Yield sources at the end
|
|
yield { type: 'sources', data: documentsToSources(relevantDocs) }
|
|
}
|