Запрос на слияние 'sber_mobile' (#22) из sber_mobile в main
This commit is contained in:
2357
package-lock.json
generated
2357
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,9 @@
|
|||||||
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
|
"@langchain/community": "^0.3.41",
|
||||||
|
"@langchain/core": "^0.3.46",
|
||||||
|
"@langchain/langgraph": "^0.2.65",
|
||||||
"ai": "^4.1.13",
|
"ai": "^4.1.13",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
@@ -34,8 +37,11 @@
|
|||||||
"express": "5.0.1",
|
"express": "5.0.1",
|
||||||
"express-jwt": "^8.5.1",
|
"express-jwt": "^8.5.1",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
"gigachat": "^0.0.14",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"langchain": "^0.3.7",
|
||||||
|
"langchain-gigachat": "^0.0.11",
|
||||||
"mongodb": "^6.12.0",
|
"mongodb": "^6.12.0",
|
||||||
"mongoose": "^8.9.2",
|
"mongoose": "^8.9.2",
|
||||||
"mongoose-sequence": "^6.0.1",
|
"mongoose-sequence": "^6.0.1",
|
||||||
@@ -44,7 +50,7 @@
|
|||||||
"pbkdf2-password": "^1.2.1",
|
"pbkdf2-password": "^1.2.1",
|
||||||
"rotating-file-stream": "^3.2.5",
|
"rotating-file-stream": "^3.2.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^11.0.3"
|
"zod": "^3.24.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ CREATE TABLE payment_service_details (
|
|||||||
CREATE TABLE tickets (
|
CREATE TABLE tickets (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||||
building_id UUID NOT NULL REFERENCES buildings(id),
|
apartment_id UUID NOT NULL REFERENCES apartments(id),
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')),
|
status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')),
|
||||||
@@ -197,6 +197,7 @@ CREATE INDEX idx_votes_initiative ON votes(initiative_id);
|
|||||||
CREATE INDEX idx_messages_chat ON messages(chat_id);
|
CREATE INDEX idx_messages_chat ON messages(chat_id);
|
||||||
CREATE INDEX idx_cameras_building ON cameras(building_id);
|
CREATE INDEX idx_cameras_building ON cameras(building_id);
|
||||||
CREATE INDEX idx_tickets_user ON tickets(user_id);
|
CREATE INDEX idx_tickets_user ON tickets(user_id);
|
||||||
|
CREATE INDEX idx_tickets_apartment ON tickets(apartment_id);
|
||||||
CREATE INDEX idx_apartments_building ON apartments(building_id);
|
CREATE INDEX idx_apartments_building ON apartments(building_id);
|
||||||
CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id);
|
CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id);
|
||||||
CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id);
|
CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
const getSupabaseUrl = async () => {
|
const getSupabaseUrl = async () => {
|
||||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -18,8 +16,74 @@ const getSupabaseServiceKey = async () => {
|
|||||||
return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value;
|
return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGigaAuth = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].GIGA_AUTH.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLangsmithApiKey = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].LANGSMITH_API_KEY.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLangsmithEndpoint = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].LANGSMITH_ENDPOINT.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLangsmithTracing = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].LANGSMITH_TRACING.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLangsmithProject = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].LANGSMITH_PROJECT.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTavilyApiKey = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].TAVILY_API_KEY.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRagSupabaseServiceRoleKey = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].RAG_SUPABASE_SERVICE_ROLE_KEY.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRagSupabaseUrl = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.features['sber_mobile'].RAG_SUPABASE_URL.value;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSupabaseUrl,
|
getSupabaseUrl,
|
||||||
getSupabaseKey,
|
getSupabaseKey,
|
||||||
getSupabaseServiceKey
|
getSupabaseServiceKey
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// IIFE для установки переменных окружения
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
process.env.GIGA_AUTH = await getGigaAuth();
|
||||||
|
process.env.LANGSMITH_API_KEY = await getLangsmithApiKey();
|
||||||
|
process.env.LANGSMITH_ENDPOINT = await getLangsmithEndpoint();
|
||||||
|
process.env.LANGSMITH_TRACING = await getLangsmithTracing();
|
||||||
|
process.env.LANGSMITH_PROJECT = await getLangsmithProject();
|
||||||
|
process.env.TAVILY_API_KEY = await getTavilyApiKey();
|
||||||
|
process.env.RAG_SUPABASE_SERVICE_ROLE_KEY = await getRagSupabaseServiceRoleKey();
|
||||||
|
process.env.RAG_SUPABASE_URL = await getRagSupabaseUrl();
|
||||||
|
|
||||||
|
console.log('Environment variables loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading environment variables:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||||
|
import { getSupabaseClient } from '../supabaseClient';
|
||||||
|
|
||||||
|
export class CreateTicketTool extends StructuredTool {
|
||||||
|
name = 'create_ticket';
|
||||||
|
description = 'Создает заявку в системе. ВАЖНО: используй этот инструмент ТОЛЬКО после получения явного согласия пользователя на создание заявки с конкретным текстом.';
|
||||||
|
|
||||||
|
schema = z.object({
|
||||||
|
title: z.string().describe('Заголовок заявки'),
|
||||||
|
description: z.string().describe('Подробное описание проблемы'),
|
||||||
|
category: z.string().describe('Категория заявки (например: ремонт, уборка, техническая_поддержка, жалоба)'),
|
||||||
|
});
|
||||||
|
|
||||||
|
private userId: string;
|
||||||
|
private apartmentId: string;
|
||||||
|
|
||||||
|
constructor(userId: string, apartmentId: string) {
|
||||||
|
super();
|
||||||
|
this.userId = userId;
|
||||||
|
this.apartmentId = apartmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _call(
|
||||||
|
arg: z.infer<typeof this.schema>,
|
||||||
|
runManager?: CallbackManagerForToolRun,
|
||||||
|
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (!this.apartmentId) {
|
||||||
|
return 'Не удалось определить вашу квартиру. Обратитесь к администратору для создания заявки.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getSupabaseClient();
|
||||||
|
|
||||||
|
const { data: ticket, error } = await supabase
|
||||||
|
.from('tickets')
|
||||||
|
.insert({
|
||||||
|
user_id: this.userId,
|
||||||
|
apartment_id: this.apartmentId,
|
||||||
|
title: arg.title,
|
||||||
|
description: arg.description,
|
||||||
|
category: arg.category,
|
||||||
|
status: 'open'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return 'Произошла ошибка при создании заявки. Попробуйте позже или обратитесь к администратору.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Заявка успешно создана!
|
||||||
|
Номер заявки: ${ticket.id}
|
||||||
|
Заголовок: ${ticket.title}
|
||||||
|
Статус: Открыта
|
||||||
|
Дата создания: ${new Date(ticket.created_at).toLocaleString('ru-RU')}
|
||||||
|
|
||||||
|
Ваша заявка принята в работу. Мы свяжемся с вами в ближайшее время.`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return 'Произошла техническая ошибка при создании заявки. Пожалуйста, попробуйте позже.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Agent } from 'node:https';
|
||||||
|
import { GigaChat } from 'langchain-gigachat';
|
||||||
|
|
||||||
|
const httpsAgent = new Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js)
|
||||||
|
export const gigachat = new GigaChat({
|
||||||
|
model: 'GigaChat-2',
|
||||||
|
temperature: 0.7,
|
||||||
|
scope: 'GIGACHAT_API_PERS',
|
||||||
|
streaming: false,
|
||||||
|
credentials: process.env.GIGA_AUTH,
|
||||||
|
httpsAgent
|
||||||
|
});
|
||||||
|
|
||||||
|
export default gigachat;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||||
|
import { getVectorStore } from './vector-store';
|
||||||
|
|
||||||
|
export class KnowledgeBaseTool extends StructuredTool {
|
||||||
|
name = 'search_knowledge_base';
|
||||||
|
description = 'Ищет информацию в базе знаний компании о процессах, оплатах, подаче заявок, правилах и документах УК. Используй этот инструмент для вопросов, требующих специфических знаний о компании.';
|
||||||
|
|
||||||
|
schema = z.object({
|
||||||
|
query: z.string().describe('Поисковый запрос для поиска в базе знаний'),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async _call(
|
||||||
|
arg: z.infer<typeof this.schema>,
|
||||||
|
runManager?: CallbackManagerForToolRun,
|
||||||
|
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const vectorStore = getVectorStore();
|
||||||
|
const retriever = vectorStore.asRetriever({
|
||||||
|
k: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
const relevantDocs = await retriever.getRelevantDocuments(arg.query);
|
||||||
|
|
||||||
|
if (!relevantDocs || relevantDocs.length === 0) {
|
||||||
|
return 'В базе знаний не найдено информации по данному запросу. Возможно, стоит переформулировать вопрос или обратиться к специалисту.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDocs = relevantDocs.map((doc, index) => {
|
||||||
|
return `Документ ${index + 1}:\n${doc.pageContent}\n`;
|
||||||
|
}).join('\n---\n');
|
||||||
|
|
||||||
|
return `Найдена следующая информация в базе знаний компании:\n\n${formattedDocs}\n\nИспользуй эту информацию для ответа на вопрос пользователя.`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return 'Произошла ошибка при поиске в базе знаний. Попробуйте переформулировать запрос.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from '@langchain/core/messages';
|
||||||
|
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
|
||||||
|
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||||
|
import { MemorySaver } from '@langchain/langgraph';
|
||||||
|
import gigachat from './gigachat';
|
||||||
|
import { SupportContextTool } from './support-context-tool';
|
||||||
|
import { KnowledgeBaseTool } from './knowledge-base-tool';
|
||||||
|
import { CreateTicketTool } from './create-ticket-tool';
|
||||||
|
|
||||||
|
export interface SupportAgentConfig {
|
||||||
|
temperature?: number;
|
||||||
|
threadId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupportResponse {
|
||||||
|
content: string;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SupportAgent {
|
||||||
|
private llm: any;
|
||||||
|
private memorySaver: MemorySaver;
|
||||||
|
private agent: any;
|
||||||
|
private systemPrompt: string;
|
||||||
|
private threadId: string;
|
||||||
|
private isFirstMessage: boolean;
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
constructor(config: SupportAgentConfig = {}) {
|
||||||
|
this.systemPrompt = this.getDefaultSystemPrompt();
|
||||||
|
this.threadId = config.threadId || 'default';
|
||||||
|
this.userId = this.threadId;
|
||||||
|
this.memorySaver = new MemorySaver();
|
||||||
|
this.isFirstMessage = true;
|
||||||
|
|
||||||
|
this.llm = gigachat;
|
||||||
|
if (config.temperature !== undefined) {
|
||||||
|
this.llm.temperature = config.temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
new SupportContextTool(this.userId),
|
||||||
|
new KnowledgeBaseTool()
|
||||||
|
];
|
||||||
|
|
||||||
|
this.agent = createReactAgent({
|
||||||
|
llm: this.llm,
|
||||||
|
tools: tools,
|
||||||
|
checkpointSaver: this.memorySaver
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultSystemPrompt(): string {
|
||||||
|
return `Ты - профессиональный агент службы поддержки управляющей компании.
|
||||||
|
|
||||||
|
ОСНОВНЫЕ ПРИНЦИПЫ:
|
||||||
|
- Помогай только с реальными проблемами и вопросами, связанными с ЖКХ, управляющей компанией и приложением
|
||||||
|
- Будь вежливым, профессиональным и по существу
|
||||||
|
- Если вопрос неуместен, не связан с твоими обязанностями или является развлекательным - вежливо откажись и перенаправь к основным темам
|
||||||
|
|
||||||
|
ДОСТУПНЫЕ ИНСТРУМЕНТЫ:
|
||||||
|
|
||||||
|
1. get_support_context - получает историю сообщений пользователя
|
||||||
|
ВСЕГДА используй ПЕРВЫМ при каждом новом сообщении
|
||||||
|
|
||||||
|
2. search_knowledge_base - поиск в базе знаний компании
|
||||||
|
Используй ТОЛЬКО для серьезных вопросов о:
|
||||||
|
- Процессах оплаты ЖКХ и тарифах
|
||||||
|
- Подаче заявок и документообороте
|
||||||
|
- Правилах и регламентах УК
|
||||||
|
- Технических вопросах приложения
|
||||||
|
- Процедурах и инструкциях компании
|
||||||
|
|
||||||
|
3. create_ticket - создание заявки в системе
|
||||||
|
Используй ТОЛЬКО когда:
|
||||||
|
- Пользователь сообщает о реальной проблеме (поломка, неисправность, жалоба)
|
||||||
|
- Проблема требует вмешательства УК или технических служб
|
||||||
|
- ОБЯЗАТЕЛЬНО сначала покажи пользователю полный текст заявки
|
||||||
|
- Получи ЯВНОЕ согласие пользователя перед созданием
|
||||||
|
- НЕ создавай заявки для консультационных вопросов
|
||||||
|
|
||||||
|
ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ:
|
||||||
|
- НЕ используй search_knowledge_base и create_ticket для:
|
||||||
|
* Общих вопросов и болтовни
|
||||||
|
* Развлекательных запросов
|
||||||
|
* Вопросов не по теме ЖКХ/УК
|
||||||
|
* Простых консультаций, которые можно решить обычным ответом
|
||||||
|
|
||||||
|
АЛГОРИТМ РАБОТЫ:
|
||||||
|
1. Получи контекст истории сообщений
|
||||||
|
2. Определи, является ли вопрос уместным и серьезным
|
||||||
|
3. Если нужна специфическая информация - найди в базе знаний
|
||||||
|
4. Если нужно создать заявку - покажи текст и получи согласие
|
||||||
|
5. Дай полный и полезный ответ
|
||||||
|
|
||||||
|
Всегда отвечай на русском языке и фокусируйся на помощи с реальными проблемами ЖКХ.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async processMessage(userMessage: string, apartmentId?: string): Promise<SupportResponse> {
|
||||||
|
try {
|
||||||
|
const messages: BaseMessage[] = [];
|
||||||
|
|
||||||
|
if (this.isFirstMessage) {
|
||||||
|
messages.push(new SystemMessage(this.systemPrompt));
|
||||||
|
this.isFirstMessage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push(new HumanMessage(userMessage));
|
||||||
|
|
||||||
|
// Создаем инструменты с актуальным apartmentId
|
||||||
|
const tools = [
|
||||||
|
new SupportContextTool(this.userId),
|
||||||
|
new KnowledgeBaseTool(),
|
||||||
|
new CreateTicketTool(this.userId, apartmentId || '')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Пересоздаем агента с обновленными инструментами
|
||||||
|
const tempAgent = createReactAgent({
|
||||||
|
llm: this.llm,
|
||||||
|
tools: tools,
|
||||||
|
checkpointSaver: this.memorySaver
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await tempAgent.invoke({
|
||||||
|
messages: messages
|
||||||
|
}, {
|
||||||
|
configurable: {
|
||||||
|
thread_id: this.threadId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastMessage = response.messages[response.messages.length - 1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: typeof lastMessage.content === 'string' ? lastMessage.content : 'Извините, не удалось сформировать ответ.',
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при обработке сообщения:', error);
|
||||||
|
return {
|
||||||
|
content: 'Извините, произошла ошибка при обработке вашего запроса. Попробуйте позже.',
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clearHistory(): Promise<void> {
|
||||||
|
this.memorySaver = new MemorySaver();
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
new SupportContextTool(this.userId),
|
||||||
|
new KnowledgeBaseTool()
|
||||||
|
];
|
||||||
|
|
||||||
|
this.agent = createReactAgent({
|
||||||
|
llm: this.llm,
|
||||||
|
tools: tools,
|
||||||
|
checkpointSaver: this.memorySaver
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isFirstMessage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||||
|
import { getSupabaseClient } from '../supabaseClient';
|
||||||
|
|
||||||
|
export class SupportContextTool extends StructuredTool {
|
||||||
|
name = 'get_support_context';
|
||||||
|
description = 'Получает последние 10 сообщений из истории поддержки для понимания контекста разговора. Используй этот инструмент в начале разговора.';
|
||||||
|
|
||||||
|
schema = z.object({});
|
||||||
|
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
constructor(userId: string) {
|
||||||
|
super();
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _call(
|
||||||
|
arg: z.infer<typeof this.schema>,
|
||||||
|
runManager?: CallbackManagerForToolRun,
|
||||||
|
parentConfig?: ToolRunnableConfig<Record<string, any>>
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const supabase = getSupabaseClient();
|
||||||
|
|
||||||
|
const { data: messages, error } = await supabase
|
||||||
|
.from('support')
|
||||||
|
.select('message, is_from_user, created_at')
|
||||||
|
.eq('user_id', this.userId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return 'Не удалось получить историю сообщений.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages || messages.length === 0) {
|
||||||
|
return 'История сообщений поддержки пуста. Это первое обращение пользователя.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const chronologicalMessages = messages.reverse();
|
||||||
|
|
||||||
|
const contextMessages = chronologicalMessages.map((msg, index) => {
|
||||||
|
const role = msg.is_from_user ? 'Пользователь' : 'Агент поддержки';
|
||||||
|
const time = new Date(msg.created_at).toLocaleString('ru-RU');
|
||||||
|
return `${index + 1}. [${time}] ${role}: ${msg.message}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
return `Последние сообщения из истории поддержки (${messages.length} сообщений):\n\n${contextMessages}\n\nИспользуй этот контекст для понимания предыдущих обращений пользователя и предоставления более точных ответов.`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return 'Произошла ошибка при получении истории сообщений.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
|
||||||
|
import { GigaChatEmbeddings } from 'langchain-gigachat';
|
||||||
|
import { Agent } from 'node:https';
|
||||||
|
|
||||||
|
const httpsAgent = new Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let vectorStoreInstance: SupabaseVectorStore | null = null;
|
||||||
|
|
||||||
|
export function getVectorStore(): SupabaseVectorStore {
|
||||||
|
if (!vectorStoreInstance) {
|
||||||
|
const client = createClient(
|
||||||
|
process.env.RAG_SUPABASE_URL!,
|
||||||
|
process.env.RAG_SUPABASE_SERVICE_ROLE_KEY!,
|
||||||
|
);
|
||||||
|
|
||||||
|
vectorStoreInstance = new SupabaseVectorStore(
|
||||||
|
new GigaChatEmbeddings({
|
||||||
|
credentials: process.env.GIGA_AUTH,
|
||||||
|
httpsAgent,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
client,
|
||||||
|
tableName: 'slon',
|
||||||
|
queryName: 'match_slon'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vectorStoreInstance;
|
||||||
|
}
|
||||||
@@ -1,16 +1,148 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const { getSupabaseClient } = require('./supabaseClient');
|
const { getSupabaseClient } = require('./supabaseClient');
|
||||||
|
const { SupportAgent } = require('./support-ai-agent/support-agent');
|
||||||
|
|
||||||
|
// Хранилище агентов для разных пользователей
|
||||||
|
const userAgents = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить или создать агента для пользователя
|
||||||
|
*/
|
||||||
|
function getUserAgent(userId) {
|
||||||
|
if (!userAgents.has(userId)) {
|
||||||
|
const config = {
|
||||||
|
threadId: userId,
|
||||||
|
temperature: 0.7
|
||||||
|
};
|
||||||
|
userAgents.set(userId, new SupportAgent(config));
|
||||||
|
}
|
||||||
|
return userAgents.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/support - Получить историю сообщений пользователя
|
||||||
|
router.get('/support', async (req, res) => {
|
||||||
|
const supabase = getSupabaseClient();
|
||||||
|
const { user_id } = req.query;
|
||||||
|
|
||||||
|
if (!user_id) {
|
||||||
|
return res.status(400).json({ error: 'user_id обязателен' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем все сообщения пользователя из базы данных
|
||||||
|
const { data: messages, error } = await supabase
|
||||||
|
.from('support')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user_id)
|
||||||
|
.order('created_at', { ascending: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
messages: messages || [],
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка в GET /support:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Внутренняя ошибка сервера',
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// POST /api/support
|
// POST /api/support
|
||||||
router.post('/support', async (req, res) => {
|
router.post('/support', async (req, res) => {
|
||||||
const supabase = getSupabaseClient();
|
const supabase = getSupabaseClient();
|
||||||
const { user_id, message } = req.body;
|
const { user_id, message, apartment_id } = req.body;
|
||||||
if (!user_id || !message) return res.status(400).json({ error: 'user_id и message обязательны' });
|
|
||||||
const { error } = await supabase
|
if (!user_id || !message) {
|
||||||
.from('support')
|
return res.status(400).json({ error: 'user_id и message обязательны' });
|
||||||
.insert({ user_id, message, is_from_user: true });
|
}
|
||||||
if (error) return res.status(400).json({ error: error.message });
|
|
||||||
res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' });
|
try {
|
||||||
|
// Сохраняем сообщение пользователя в базу данных
|
||||||
|
const { error: insertError } = await supabase
|
||||||
|
.from('support')
|
||||||
|
.insert({ user_id, message, is_from_user: true });
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
return res.status(400).json({ error: insertError.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем агента для пользователя
|
||||||
|
const agent = getUserAgent(user_id);
|
||||||
|
|
||||||
|
// Получаем ответ от AI-агента, передавая apartment_id
|
||||||
|
const aiResponse = await agent.processMessage(message, apartment_id);
|
||||||
|
|
||||||
|
if (!aiResponse.success) {
|
||||||
|
console.error('Ошибка AI-агента:', aiResponse.error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Ошибка при генерации ответа',
|
||||||
|
reply: 'Извините, произошла ошибка. Попробуйте позже.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем ответ агента в базу данных
|
||||||
|
const { error: responseError } = await supabase
|
||||||
|
.from('support')
|
||||||
|
.insert({
|
||||||
|
user_id,
|
||||||
|
message: aiResponse.content,
|
||||||
|
is_from_user: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseError) {
|
||||||
|
console.error('Ошибка сохранения ответа:', responseError);
|
||||||
|
// Не возвращаем ошибку пользователю, так как ответ уже сгенерирован
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем ответ пользователю
|
||||||
|
res.json({
|
||||||
|
reply: aiResponse.content,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка в supportApi:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Внутренняя ошибка сервера',
|
||||||
|
reply: 'Извините, произошла ошибка. Попробуйте позже.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/support/history/:userId - Очистка истории диалога
|
||||||
|
router.delete('/support/history/:userId', async (req, res) => {
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (userAgents.has(userId)) {
|
||||||
|
const agent = userAgents.get(userId);
|
||||||
|
await agent.clearHistory();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'История диалога очищена',
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
message: 'Агент для данного пользователя не найден',
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка в /support/history:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Внутренняя ошибка сервера',
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user