ваше сообщение коммита

This commit is contained in:
2025-07-03 21:54:00 +03:00
parent 11cf0ea353
commit 972553dbb1
21 changed files with 1435 additions and 176 deletions

View File

@@ -1,17 +1,13 @@
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
const db = require('../db');
const { ChatOllama } = require('@langchain/ollama');
const { OllamaEmbeddings } = require('@langchain/ollama');
const vectorSearch = require('./vectorSearchClient');
const { getProviderSettings } = require('./aiProviderSettingsService');
const { OpenAIEmbeddings } = require('@langchain/openai');
console.log('[RAG] ragService.js loaded');
async function getTableData(tableId) {
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
console.log('RAG getTableData: columns:', columns);
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const columns = (await db.query('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
const rows = (await db.query('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const cellValues = (await db.query('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
const questionColId = getColId('question');
@@ -35,147 +31,142 @@ async function getTableData(tableId) {
date: cells.find(c => c.column_id === dateColId)?.value,
};
});
const questions = data.map(row => row.question);
console.log('RAG getTableData: questions:', questions);
if (!questions.length) {
console.warn('RAG getTableData: questions array is empty! Проверьте структуру колонок и наличие данных.');
}
return data;
}
async function getEmbeddingsProvider(providerName = 'ollama') {
const settings = await getProviderSettings(providerName);
if (!settings) throw new Error('Embeddings provider settings not found');
switch (providerName) {
case 'openai':
return new OpenAIEmbeddings({
apiKey: settings.api_key,
baseURL: settings.base_url,
model: settings.selected_model || undefined,
});
case 'ollama': {
// Fallback: если не задан base_url, пробуем env, host.docker.internal, localhost
let baseUrl = settings.base_url;
if (!baseUrl) {
baseUrl = process.env.OLLAMA_BASE_URL;
}
if (!baseUrl) {
// Если в Docker — используем host.docker.internal
baseUrl = 'http://host.docker.internal:11434';
}
// Если всё равно нет — последний fallback
if (!baseUrl) {
baseUrl = 'http://localhost:11434';
}
return new OllamaEmbeddings({
model: settings.embedding_model || process.env.OLLAMA_EMBED_MODEL || 'mxbai-embed-large',
baseUrl,
});
}
// case 'gemini':
// return new GeminiEmbeddings({ apiKey: settings.api_key });
// Добавьте другие провайдеры по аналогии
default:
throw new Error('Unknown embeddings provider: ' + providerName);
}
}
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null, embeddingsProvider = 'ollama', threshold = 0.3 }) {
console.log('[RAG] Используется провайдер эмбеддингов:', embeddingsProvider);
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null, threshold = 0.3 }) {
console.log(`[RAG] ragAnswer called: tableId=${tableId}, userQuestion="${userQuestion}"`);
const data = await getTableData(tableId);
// Триммируем вопросы для чистоты сравнения
console.log(`[RAG] Got ${data.length} rows from database`);
const questions = data.map(row => row.question && typeof row.question === 'string' ? row.question.trim() : row.question);
// Получаем embeddings-инстанс динамически
const embeddingsInstance = await getEmbeddingsProvider(embeddingsProvider);
// Получаем embedding для всех вопросов
const embeddings = await embeddingsInstance.embedDocuments(questions);
console.log('Questions embedding length:', embeddings[0]?.length, 'Total questions:', questions.length);
// Получаем embedding для вопроса пользователя (trim)
const userQuestionTrimmed = userQuestion && typeof userQuestion === 'string' ? userQuestion.trim() : userQuestion;
const [userEmbedding] = await embeddingsInstance.embedDocuments([userQuestionTrimmed]);
console.log('User embedding length:', userEmbedding?.length, 'User question:', userQuestionTrimmed);
// Явно сравниваем embeddings (отладка)
console.log('[RAG] Embedding сравнение:');
embeddings.forEach((emb, idx) => {
const dot = emb.reduce((sum, v, i) => sum + v * userEmbedding[i], 0);
console.log(` [${idx}] dot-product: ${dot} | question: "${questions[idx]}"`);
});
// Создаём массив метаданных для каждого вопроса
const metadatas = data.map(row => ({
id: row.id,
answer: row.answer,
userTags: row.userTags,
context: row.context,
product: row.product,
priority: row.priority,
date: row.date,
const rowsForUpsert = data.map(row => ({
row_id: row.id,
text: row.question,
metadata: {
answer: row.answer || null,
userTags: row.userTags || null,
context: row.context || null,
product: row.product || null,
priority: row.priority || null,
date: row.date || null
}
}));
// Создаём векторное хранилище
const vectorStore = await HNSWLib.fromTexts(questions, metadatas, embeddingsInstance);
// Ищем наиболее похожие вопросы (top-3)
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
console.log('[RAG] Результаты поиска по векторам (score):', results.map(([doc, score]) => ({ ...doc.metadata, score })));
// Фильтруем по тегам/продукту, если нужно
let filtered = results.map(([doc, score]) => ({ ...doc.metadata, score }));
console.log(`[RAG] Prepared ${rowsForUpsert.length} rows for upsert`);
console.log(`[RAG] First row:`, rowsForUpsert[0]);
// Upsert все вопросы в индекс (можно оптимизировать по изменению)
if (rowsForUpsert.length > 0) {
await vectorSearch.upsert(tableId, rowsForUpsert);
console.log(`[RAG] Upsert completed`);
} else {
console.log(`[RAG] No rows to upsert, skipping`);
}
// Поиск
let results = [];
if (rowsForUpsert.length > 0) {
results = await vectorSearch.search(tableId, userQuestion, 3);
console.log(`[RAG] Search completed, got ${results.length} results`);
} else {
console.log(`[RAG] No data in table, skipping search`);
}
// Фильтрация по тегам/продукту
let filtered = results;
if (userTags.length) {
filtered = filtered.filter(row => row.userTags && userTags.some(tag => row.userTags.includes(tag)));
filtered = filtered.filter(row => row.metadata.userTags && userTags.some(tag => row.metadata.userTags.includes(tag)));
}
if (product) {
filtered = filtered.filter(row => row.product === product);
filtered = filtered.filter(row => row.metadata.product === product);
}
console.log('[RAG] Отфильтрованные результаты:', filtered);
// Берём лучший результат с учётом порога
const best = filtered.find(row => row.score >= threshold);
console.log(`[RAG] Выбранный ответ (порог ${threshold}):`, best);
// Формируем ответ
console.log(`[RAG] Best result:`, best);
return {
answer: best?.answer,
context: best?.context,
product: best?.product,
priority: best?.priority,
date: best?.date,
answer: best?.metadata?.answer,
context: best?.metadata?.context,
product: best?.metadata?.product,
priority: best?.metadata?.priority,
date: best?.metadata?.date,
score: best?.score,
};
}
async function generateLLMResponse({ userQuestion, context, clarifyingAnswer, objectionAnswer, answer, systemPrompt, userTags, product, priority, date, rules, history, model, language }) {
// Подставляем значения в шаблон промта
let prompt = (systemPrompt || '')
.replace('{context}', context || '')
.replace('{clarifyingAnswer}', clarifyingAnswer || '')
.replace('{objectionAnswer}', objectionAnswer || '')
.replace('{answer}', answer || '')
.replace('{question}', userQuestion || '')
.replace('{userTags}', userTags || '')
.replace('{product}', product || '')
.replace('{priority}', priority || '')
.replace('{date}', date || '')
.replace('{rules}', rules || '')
.replace('{history}', history || '')
.replace('{model}', model || '')
.replace('{language}', language || '');
const chat = new ChatOllama({
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
model: process.env.OLLAMA_MODEL || 'qwen2.5',
system: prompt,
temperature: 0.7,
maxTokens: 1000,
timeout: 30000,
async function generateLLMResponse({
userQuestion,
context,
clarifyingAnswer,
objectionAnswer,
answer,
systemPrompt,
userTags,
product,
priority,
date,
rules,
history,
model,
language
}) {
console.log(`[RAG] generateLLMResponse called with:`, {
userQuestion,
context,
answer,
systemPrompt,
userTags,
product,
priority,
date,
model,
language
});
const response = await chat.invoke(`Вопрос пользователя: ${userQuestion}`);
return response.content;
try {
const aiAssistant = require('./ai-assistant');
// Формируем промпт для LLM
let prompt = userQuestion;
if (context) {
prompt += `\n\nКонтекст: ${context}`;
}
if (answer) {
prompt += `\n\nНайденный ответ: ${answer}`;
}
if (userTags) {
prompt += `\n\nТеги: ${userTags}`;
}
if (product) {
prompt += `\n\nПродукт: ${product}`;
}
// Получаем ответ от AI
const llmResponse = await aiAssistant.getResponse(
prompt,
language || 'auto',
history,
systemPrompt,
rules
);
console.log(`[RAG] LLM response generated:`, llmResponse);
return llmResponse;
} catch (error) {
console.error(`[RAG] Error generating LLM response:`, error);
return 'Извините, произошла ошибка при генерации ответа.';
}
}
module.exports = { ragAnswer, generateLLMResponse };
module.exports = {
ragAnswer,
getTableData,
generateLLMResponse
};