ваше сообщение коммита
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
FROM node:20-bullseye
|
||||
FROM node:20-bookworm as nodebase
|
||||
|
||||
FROM ubuntu:24.04
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем зависимости, включая Python для node-gyp
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 make g++ cmake openssl libssl-dev \
|
||||
ca-certificates curl gnupg lsb-release
|
||||
ca-certificates curl gnupg lsb-release \
|
||||
build-essential python3-dev libc6-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# Устанавливаем Node.js 20 и yarn
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
npm install -g yarn
|
||||
|
||||
RUN echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# Копируем libnode.so.109 из node:20-bookworm
|
||||
COPY --from=nodebase /usr/lib/x86_64-linux-gnu/libnode.so.109 /usr/lib/x86_64-linux-gnu/libnode.so.109
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
# Устанавливаем node-gyp глобально
|
||||
RUN npm install -g node-gyp
|
||||
|
||||
# Копируем package.json и yarn.lock для установки зависимостей
|
||||
COPY package.json yarn.lock ./
|
||||
@@ -23,6 +28,16 @@ COPY package.json yarn.lock ./
|
||||
# Устанавливаем зависимости
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# ПРИНУДИТЕЛЬНО пересобираем hnswlib-node из исходников
|
||||
RUN echo "Пересобираем hnswlib-node из исходников..." && \
|
||||
cd node_modules/hnswlib-node && \
|
||||
rm -rf build/ && \
|
||||
node-gyp rebuild --verbose
|
||||
|
||||
# Проверяем версию libstdc++ и наличие нужного символа
|
||||
RUN echo "Версия libstdc++:" && strings /lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX | tail -5
|
||||
RUN echo "Проверка GLIBCXX_3.4.32:" && strings /lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX_3.4.32 || echo "GLIBCXX_3.4.32 not found"
|
||||
|
||||
# Копируем остальные файлы проекта
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const tagsInitRoutes = require('./routes/tagsInit');
|
||||
const tagsRoutes = require('./routes/tags');
|
||||
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
|
||||
const cloudflareRoutes = require('./routes/cloudflare');
|
||||
const monitoringRoutes = require('./routes/monitoring');
|
||||
|
||||
// Проверка и создание директорий для хранения данных контрактов
|
||||
const ensureDirectoriesExist = () => {
|
||||
@@ -192,6 +193,7 @@ app.use('/api/tags', tagsRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||
app.use('/api/cloudflare', cloudflareRoutes);
|
||||
app.use('/api/monitoring', monitoringRoutes);
|
||||
|
||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||
|
||||
@@ -221,18 +223,48 @@ app.use(errorHandler);
|
||||
// Эндпоинт для проверки состояния
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
// Проверяем подключение к БД
|
||||
await db.getQuery('SELECT NOW()');
|
||||
|
||||
// Проверяем AI сервис
|
||||
const aiStatus = await aiAssistant.checkHealth();
|
||||
|
||||
res.json({
|
||||
const healthStatus = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: 'connected',
|
||||
ai: aiStatus,
|
||||
});
|
||||
services: {}
|
||||
};
|
||||
|
||||
// Проверяем подключение к БД
|
||||
try {
|
||||
await db.query('SELECT NOW()');
|
||||
healthStatus.services.database = { status: 'ok' };
|
||||
} catch (error) {
|
||||
healthStatus.services.database = { status: 'error', error: error.message };
|
||||
healthStatus.status = 'error';
|
||||
}
|
||||
|
||||
// Проверяем AI сервис
|
||||
try {
|
||||
const aiStatus = await aiAssistant.checkHealth();
|
||||
healthStatus.services.ai = aiStatus;
|
||||
if (aiStatus.status === 'error') {
|
||||
healthStatus.status = 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
healthStatus.services.ai = { status: 'error', error: error.message };
|
||||
healthStatus.status = 'error';
|
||||
}
|
||||
|
||||
// Проверяем Vector Search сервис
|
||||
try {
|
||||
const vectorSearchClient = require('./services/vectorSearchClient');
|
||||
const vectorStatus = await vectorSearchClient.health();
|
||||
healthStatus.services.vectorSearch = vectorStatus;
|
||||
if (vectorStatus.status === 'error') {
|
||||
healthStatus.status = 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
healthStatus.services.vectorSearch = { status: 'error', error: error.message };
|
||||
healthStatus.status = 'error';
|
||||
}
|
||||
|
||||
const statusCode = healthStatus.status === 'ok' ? 200 : 503;
|
||||
res.status(statusCode).json(healthStatus);
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
res.status(500).json({
|
||||
|
||||
38
backend/routes/monitoring.js
Normal file
38
backend/routes/monitoring.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const db = require('../db');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const results = {};
|
||||
// Backend
|
||||
results.backend = { status: 'ok' };
|
||||
|
||||
// Vector Search
|
||||
try {
|
||||
const vs = await axios.get(process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001/health', { timeout: 2000 });
|
||||
results.vectorSearch = { status: 'ok', ...vs.data };
|
||||
} catch (e) {
|
||||
results.vectorSearch = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
// Ollama
|
||||
try {
|
||||
const ollama = await axios.get(process.env.OLLAMA_BASE_URL ? process.env.OLLAMA_BASE_URL + '/api/tags' : 'http://ollama:11434/api/tags', { timeout: 2000 });
|
||||
results.ollama = { status: 'ok', models: ollama.data.models?.length || 0 };
|
||||
} catch (e) {
|
||||
results.ollama = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
// PostgreSQL
|
||||
try {
|
||||
await db.query('SELECT 1');
|
||||
results.postgres = { status: 'ok' };
|
||||
} catch (e) {
|
||||
results.postgres = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -138,6 +138,29 @@ class AIAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка здоровья AI сервиса
|
||||
async checkHealth() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API returned ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
status: 'ok',
|
||||
models: data.models?.length || 0,
|
||||
baseUrl: this.baseUrl
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('AI health check failed:', error);
|
||||
return {
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
baseUrl: this.baseUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем методы из vectorStore.js
|
||||
async initVectorStore() {
|
||||
// ... код инициализации ...
|
||||
|
||||
@@ -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
|
||||
};
|
||||
69
backend/services/vectorSearchClient.js
Normal file
69
backend/services/vectorSearchClient.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const VECTOR_SEARCH_URL = process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001';
|
||||
|
||||
async function upsert(tableId, rows) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/upsert`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function search(tableId, query, topK = 3) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/search`, {
|
||||
table_id: String(tableId),
|
||||
query,
|
||||
top_k: topK
|
||||
});
|
||||
return res.data.results;
|
||||
}
|
||||
|
||||
async function remove(tableId, rowIds) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/delete`, {
|
||||
table_id: String(tableId),
|
||||
row_ids: rowIds.map(id => String(id))
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function rebuild(tableId, rows) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/rebuild`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function health() {
|
||||
try {
|
||||
const res = await axios.get(`${VECTOR_SEARCH_URL}/health`, { timeout: 5000 });
|
||||
return {
|
||||
status: 'ok',
|
||||
url: VECTOR_SEARCH_URL,
|
||||
response: res.data
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
url: VECTOR_SEARCH_URL,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
upsert,
|
||||
search,
|
||||
remove,
|
||||
rebuild,
|
||||
health
|
||||
};
|
||||
89
backend/tests/ragService.test.js
Normal file
89
backend/tests/ragService.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// Принудительно устанавливаем URL для Docker-сети
|
||||
process.env.VECTOR_SEARCH_URL = 'http://vector-search:8001';
|
||||
|
||||
const vectorSearch = require('../services/vectorSearchClient');
|
||||
|
||||
const TEST_TABLE_ID = 'test_table_rag';
|
||||
|
||||
const rows = [
|
||||
{ row_id: '1', text: 'Что такое RAG?', metadata: { answer: 'Retrieval Augmented Generation', userTags: ['ai', 'ml'], product: 'A' } },
|
||||
{ row_id: '2', text: 'Что такое FAISS?', metadata: { answer: 'Facebook AI Similarity Search', userTags: ['ai', 'search'], product: 'B' } },
|
||||
{ row_id: '3', text: 'Что такое Ollama?', metadata: { answer: 'Локальный inference LLM', userTags: ['llm'], product: 'A' } },
|
||||
];
|
||||
|
||||
describe('vectorSearchClient integration (vector-search)', () => {
|
||||
before(async () => {
|
||||
console.log('Загружаем тестовые данные...');
|
||||
console.log('VECTOR_SEARCH_URL:', process.env.VECTOR_SEARCH_URL);
|
||||
await vectorSearch.rebuild(TEST_TABLE_ID, rows);
|
||||
console.log('Тестовые данные загружены');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
console.log('Очищаем тестовые данные...');
|
||||
await vectorSearch.remove(TEST_TABLE_ID, rows.map(r => r.row_id));
|
||||
console.log('Тестовые данные очищены');
|
||||
});
|
||||
|
||||
it('Поиск без фильтрации', async () => {
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Что такое RAG?', 1);
|
||||
console.log('Результаты поиска:', results);
|
||||
if (!results || results.length === 0) throw new Error('Нет результатов поиска');
|
||||
if (results[0].metadata.answer !== 'Retrieval Augmented Generation') {
|
||||
throw new Error(`Ответ не совпадает: ${results[0].metadata.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Поиск с фильтрацией по тегу (должен найти FAISS)', async () => {
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Что такое FAISS?', 3);
|
||||
console.log('Результаты поиска FAISS:', results);
|
||||
if (!results || results.length === 0) throw new Error('Нет результатов поиска');
|
||||
|
||||
// Фильтруем по тегу 'search'
|
||||
const filtered = results.filter(r => r.metadata.userTags && r.metadata.userTags.includes('search'));
|
||||
if (filtered.length === 0) throw new Error('Нет результатов с тегом search');
|
||||
if (filtered[0].metadata.answer !== 'Facebook AI Similarity Search') {
|
||||
throw new Error(`Ответ не совпадает: ${filtered[0].metadata.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Поиск с фильтрацией по продукту (должен найти Ollama)', async () => {
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Что такое Ollama?', 3);
|
||||
console.log('Результаты поиска Ollama:', results);
|
||||
if (!results || results.length === 0) throw new Error('Нет результатов поиска');
|
||||
|
||||
// Фильтруем по продукту 'A'
|
||||
const filtered = results.filter(r => r.metadata.product === 'A');
|
||||
if (filtered.length === 0) throw new Error('Нет результатов с продуктом A');
|
||||
if (filtered[0].metadata.answer !== 'Локальный inference LLM') {
|
||||
throw new Error(`Ответ не совпадает: ${filtered[0].metadata.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Комбинированная фильтрация (тег+продукт)', async () => {
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Что такое RAG?', 3);
|
||||
console.log('Результаты поиска RAG:', results);
|
||||
if (!results || results.length === 0) throw new Error('Нет результатов поиска');
|
||||
|
||||
// Фильтруем по тегу 'ai' и продукту 'A'
|
||||
const filtered = results.filter(r =>
|
||||
r.metadata.userTags && r.metadata.userTags.includes('ai') &&
|
||||
r.metadata.product === 'A'
|
||||
);
|
||||
if (filtered.length === 0) throw new Error('Нет результатов с тегом ai и продуктом A');
|
||||
if (filtered[0].metadata.answer !== 'Retrieval Augmented Generation') {
|
||||
throw new Error(`Ответ не совпадает: ${filtered[0].metadata.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Проверка порога score', async () => {
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Что такое Ollama?', 3);
|
||||
console.log('Результаты поиска с порогом:', results);
|
||||
if (!results || results.length === 0) throw new Error('Нет результатов поиска');
|
||||
|
||||
// Проверяем, что есть результаты с хорошим score (близкие к 0)
|
||||
const goodScoreResults = results.filter(r => Math.abs(r.score) < 10);
|
||||
if (goodScoreResults.length === 0) throw new Error('Нет результатов с хорошим score');
|
||||
console.log('Результаты с хорошим score:', goodScoreResults.length);
|
||||
});
|
||||
});
|
||||
163
backend/tests/ragServiceFull.test.js
Normal file
163
backend/tests/ragServiceFull.test.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// Временно устанавливаем URL для локального тестирования
|
||||
process.env.VECTOR_SEARCH_URL = 'http://localhost:8001';
|
||||
|
||||
const ragService = require('../services/ragService');
|
||||
const db = require('../db');
|
||||
|
||||
const TEST_TABLE_ID = 999999; // Используем числовой ID
|
||||
|
||||
describe('ragService full integration (DB + vector-search)', () => {
|
||||
before(async () => {
|
||||
console.log('Создаем тестовую таблицу и данные...');
|
||||
|
||||
// Создаем тестовую таблицу
|
||||
await db.getQuery()(`
|
||||
INSERT INTO user_tables (id, name, description)
|
||||
VALUES ($1, 'Test RAG Table', 'Test table for RAG integration')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [TEST_TABLE_ID]);
|
||||
|
||||
// Создаем колонки
|
||||
const columns = [
|
||||
{ id: 'col_question', name: 'Question', type: 'text', purpose: 'question' },
|
||||
{ id: 'col_answer', name: 'Answer', type: 'text', purpose: 'answer' },
|
||||
{ id: 'col_tags', name: 'Tags', type: 'text', purpose: 'userTags' },
|
||||
{ id: 'col_product', name: 'Product', type: 'text', purpose: 'product' }
|
||||
];
|
||||
|
||||
for (const col of columns) {
|
||||
await db.getQuery()(`
|
||||
INSERT INTO user_columns (id, table_id, name, type, options)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [col.id, TEST_TABLE_ID, col.name, col.type, JSON.stringify({ purpose: col.purpose })]);
|
||||
}
|
||||
|
||||
// Создаем строки
|
||||
const rows = [
|
||||
{ id: 'row_1', question: 'Что такое RAG?', answer: 'Retrieval Augmented Generation', tags: 'ai,ml', product: 'A' },
|
||||
{ id: 'row_2', question: 'Что такое FAISS?', answer: 'Facebook AI Similarity Search', tags: 'ai,search', product: 'B' },
|
||||
{ id: 'row_3', question: 'Что такое Ollama?', answer: 'Локальный inference LLM', tags: 'llm', product: 'A' }
|
||||
];
|
||||
|
||||
for (const row of rows) {
|
||||
// Создаем строку
|
||||
await db.getQuery()(`
|
||||
INSERT INTO user_rows (id, table_id, name)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [row.id, TEST_TABLE_ID, row.question]);
|
||||
|
||||
// Создаем значения ячеек
|
||||
await db.getQuery()(`
|
||||
INSERT INTO user_cell_values (row_id, column_id, value)
|
||||
VALUES ($1, $2, $3), ($1, $4, $5), ($1, $6, $7), ($1, $8, $9)
|
||||
ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value
|
||||
`, [
|
||||
row.id, 'col_question', row.question,
|
||||
row.id, 'col_answer', row.answer,
|
||||
row.id, 'col_tags', row.tags,
|
||||
row.id, 'col_product', row.product
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Тестовые данные созданы');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
console.log('Очищаем тестовые данные...');
|
||||
|
||||
// Удаляем значения ячеек
|
||||
await db.getQuery()(`
|
||||
DELETE FROM user_cell_values
|
||||
WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)
|
||||
`, [TEST_TABLE_ID]);
|
||||
|
||||
// Удаляем строки
|
||||
await db.getQuery()(`
|
||||
DELETE FROM user_rows WHERE table_id = $1
|
||||
`, [TEST_TABLE_ID]);
|
||||
|
||||
// Удаляем колонки
|
||||
await db.getQuery()(`
|
||||
DELETE FROM user_columns WHERE table_id = $1
|
||||
`, [TEST_TABLE_ID]);
|
||||
|
||||
// Удаляем таблицу
|
||||
await db.getQuery()(`
|
||||
DELETE FROM user_tables WHERE id = $1
|
||||
`, [TEST_TABLE_ID]);
|
||||
|
||||
console.log('Тестовые данные очищены');
|
||||
});
|
||||
|
||||
it('Полная интеграция: поиск без фильтрации', async () => {
|
||||
const result = await ragService.ragAnswer({
|
||||
tableId: TEST_TABLE_ID,
|
||||
userQuestion: 'Что такое RAG?'
|
||||
});
|
||||
|
||||
console.log('Результат RAG:', result);
|
||||
if (!result) throw new Error('Нет результата');
|
||||
if (result.answer !== 'Retrieval Augmented Generation') {
|
||||
throw new Error(`Ответ не совпадает: ${result.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Полная интеграция: фильтрация по тегу', async () => {
|
||||
const result = await ragService.ragAnswer({
|
||||
tableId: TEST_TABLE_ID,
|
||||
userQuestion: 'Что такое FAISS?',
|
||||
userTags: ['search']
|
||||
});
|
||||
|
||||
console.log('Результат с фильтром по тегу:', result);
|
||||
if (!result) throw new Error('Нет результата');
|
||||
if (result.answer !== 'Facebook AI Similarity Search') {
|
||||
throw new Error(`Ответ не совпадает: ${result.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Полная интеграция: фильтрация по продукту', async () => {
|
||||
const result = await ragService.ragAnswer({
|
||||
tableId: TEST_TABLE_ID,
|
||||
userQuestion: 'Что такое Ollama?',
|
||||
product: 'A'
|
||||
});
|
||||
|
||||
console.log('Результат с фильтром по продукту:', result);
|
||||
if (!result) throw new Error('Нет результата');
|
||||
if (result.answer !== 'Локальный inference LLM') {
|
||||
throw new Error(`Ответ не совпадает: ${result.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Полная интеграция: комбинированная фильтрация', async () => {
|
||||
const result = await ragService.ragAnswer({
|
||||
tableId: TEST_TABLE_ID,
|
||||
userQuestion: 'Что такое RAG?',
|
||||
userTags: ['ai'],
|
||||
product: 'A'
|
||||
});
|
||||
|
||||
console.log('Результат с комбинированной фильтрацией:', result);
|
||||
if (!result) throw new Error('Нет результата');
|
||||
if (result.answer !== 'Retrieval Augmented Generation') {
|
||||
throw new Error(`Ответ не совпадает: ${result.answer}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('Полная интеграция: проверка порога score', async () => {
|
||||
const result = await ragService.ragAnswer({
|
||||
tableId: TEST_TABLE_ID,
|
||||
userQuestion: 'Что такое Ollama?',
|
||||
threshold: 0.95
|
||||
});
|
||||
|
||||
console.log('Результат с высоким порогом:', result);
|
||||
// С высоким порогом может не быть результата, это нормально
|
||||
if (result && result.score < 0.95) {
|
||||
throw new Error(`Score слишком низкий: ${result.score}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
35
backend/tests/vectorSearchClient.test.js
Normal file
35
backend/tests/vectorSearchClient.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const vectorSearch = require('../services/vectorSearchClient');
|
||||
|
||||
const TEST_TABLE_ID = 'test_table_1';
|
||||
|
||||
const rows = [
|
||||
{ row_id: '1', text: 'Как тебя зовут?', metadata: { answer: 'Алексей' } },
|
||||
{ row_id: '2', text: 'Где ты живёшь?', metadata: { answer: 'Москва' } }
|
||||
];
|
||||
|
||||
describe('Vector Search Service Integration', () => {
|
||||
afterAll(async () => {
|
||||
// Очистить тестовые данные
|
||||
await vectorSearch.remove(TEST_TABLE_ID, rows.map(r => r.row_id));
|
||||
});
|
||||
|
||||
test('Upsert and search', async () => {
|
||||
await vectorSearch.upsert(TEST_TABLE_ID, rows);
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Как зовут?', 1);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].metadata.answer).toBe('Алексей');
|
||||
});
|
||||
|
||||
test('Delete', async () => {
|
||||
await vectorSearch.remove(TEST_TABLE_ID, ['1']);
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Как зовут?', 1);
|
||||
expect(results.length === 0 || results[0].metadata.answer !== 'Алексей').toBe(true);
|
||||
});
|
||||
|
||||
test('Rebuild', async () => {
|
||||
await vectorSearch.rebuild(TEST_TABLE_ID, [rows[1]]);
|
||||
const results = await vectorSearch.search(TEST_TABLE_ID, 'Где ты живёшь?', 1);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].metadata.answer).toBe('Москва');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user