Files
mkd/backend/aiToolsRegistry.js
2026-02-04 00:17:04 +05:00

313 lines
14 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Реестр инструментов ИИ-чата: описание для модели (OpenAI tools) и выполнение через self-request с JWT.
*
* КАК ДОБАВИТЬ НОВЫЙ ИНСТРУМЕНТ:
* 1. В TOOLS_FOR_OPENAI — добавить объект { type: 'function', function: { name, description, parameters } }.
* parameters — JSON Schema (properties, required, additionalProperties: false). Описание (description)
* помогает ИИ задавать наводящие вопросы и правильно заполнять аргументы.
* 2. В TOOL_EXECUTORS — добавить запись: method ('GET'|'POST'|'PUT'), pathTemplate (с :param для path),
* опционально queryParams: { argKey: queryKey }, для POST/PUT — bodyMapping: { argKey: destKey } или _createdBy: true.
* 3. runTool подставит path, query string и body, для запросов от имени пользователя вызовет наш API с JWT.
*
* Модули для расширения: финансы (payment-invoices, calendar), заявки (applications), офис (meetings, documents),
* HR (employees, vacations), юр. отдел (contracts, court-cases), развитие (pipeline, OSS).
*/
const axios = require('axios');
/** Текст «что умеет» — возвращается по команде get_capabilities */
const CAPABILITIES_TEXT = `Я помощник в системе управления МКД. Вот что я умею:
**Дома и объекты**
• Показать список всех домов (МКД) — адреса, id, основные данные.
• Показать данные по одному дому по его id (паспорт, лицевые счета и т.д.).
**Участки**
• Показать список участков (районов обслуживания) — название, руководитель.
**Сотрудники**
• Показать список сотрудников (id и имя) — для поиска исполнителя или контакта.
**Финансы — счета на оплату**
• Показать список счетов на оплату (с пагинацией: страница и количество на странице).
• Создать счёт на оплату: контрагент, сумма, назначение (мероприятие/дом/участок/прочее), формат оплаты (наличные/безнал), список услуг (название и сумма). Могу задать уточняющие вопросы, если чего-то не хватает.
**Заявки (диспетчерская)**
• Создать заявку: обязательно адрес и описание; по желанию — заявитель, квартира, срок выполнения.
Просто напиши, что нужно: например «покажи дома», «создай счёт на 10 000 для ООО Рога», «создай заявку по адресу ул. Ленина 5 — течёт кран». Если данных не хватает, я спрошу.`;
/** Описания инструментов в формате OpenAI Chat Completions (tools) */
const TOOLS_FOR_OPENAI = [
{
type: 'function',
function: {
name: 'get_capabilities',
description: 'Вернуть подробное описание всех возможностей помощника в программе МКД. Вызывай, когда пользователь спрашивает «что ты умеешь», «расскажи о возможностях», «какие команды», «помощь», «что можешь» и т.п. Аргументов нет.',
parameters: { type: 'object', properties: {}, additionalProperties: false }
}
},
{
type: 'function',
function: {
name: 'list_buildings',
description: 'Получить список всех домов (МКД). Возвращает массив домов с id, адресом и основными данными.',
parameters: { type: 'object', properties: {}, additionalProperties: false }
}
},
{
type: 'function',
function: {
name: 'get_building',
description: 'Получить данные одного дома по его id (например id из списка домов).',
parameters: {
type: 'object',
properties: {
id: { type: 'string', description: 'Идентификатор дома (id)' }
},
required: ['id'],
additionalProperties: false
}
}
},
{
type: 'function',
function: {
name: 'list_districts',
description: 'Получить список участков (районов обслуживания) с id, названием и руководителем.',
parameters: { type: 'object', properties: {}, additionalProperties: false }
}
},
{
type: 'function',
function: {
name: 'list_employees',
description: 'Получить список сотрудников (id, имя). Используй для выбора исполнителя или поиска по имени.',
parameters: { type: 'object', properties: {}, additionalProperties: false }
}
},
{
type: 'function',
function: {
name: 'list_payment_invoices',
description: 'Получить список счетов на оплату. Можно указать limit и page для пагинации.',
parameters: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Количество записей на странице (по умолчанию 10)' },
page: { type: 'number', description: 'Номер страницы (по умолчанию 1)' }
},
additionalProperties: false
}
}
},
{
type: 'function',
function: {
name: 'create_payment_invoice',
description: 'Создать счёт на оплату. Нужны: контрагент (contractorName), сумма (totalAmount), назначение (purposeType: event, building, district, other), формат оплаты (paymentFormat: cash, non_cash), список услуг (serviceItems: массив объектов { name, amount }). Опционально: purposeDescription, contractorInn, notes.',
parameters: {
type: 'object',
properties: {
contractorName: { type: 'string', description: 'Название контрагента (ИП/ООО)' },
totalAmount: { type: 'number', description: 'Общая сумма в рублях' },
purposeType: { type: 'string', enum: ['event', 'building', 'district', 'other'], description: 'Тип назначения платежа' },
paymentFormat: { type: 'string', enum: ['cash', 'non_cash'], description: 'Формат оплаты' },
serviceItems: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
amount: { type: 'number' }
},
required: ['name', 'amount']
},
description: 'Список услуг: каждый элемент { name, amount }'
},
purposeDescription: { type: 'string', description: 'Текстовое описание назначения' },
contractorInn: { type: 'string', description: 'ИНН контрагента' },
notes: { type: 'string', description: 'Заметки' }
},
required: ['contractorName', 'totalAmount', 'purposeType', 'paymentFormat', 'serviceItems'],
additionalProperties: false
}
}
},
{
type: 'function',
function: {
name: 'create_application',
description: 'Создать заявку (диспетчерская). Обязательно: адрес (address), описание (description). Опционально: contactName, apartment, deadlineAt (дата в ISO).',
parameters: {
type: 'object',
properties: {
address: { type: 'string', description: 'Адрес объекта' },
description: { type: 'string', description: 'Описание заявки' },
contactName: { type: 'string', description: 'ФИО заявителя' },
apartment: { type: 'string', description: 'Квартира/помещение' },
deadlineAt: { type: 'string', description: 'Срок выполнения (ISO дата)' }
},
required: ['address', 'description'],
additionalProperties: false
}
}
}
];
/**
* Исполнение инструмента: method, path (шаблон с :param), опционально queryParams, bodyMapping.
* queryParams: объект { argKey: queryKey } — добавляет query string из args.
* bodyMapping: для POST/PUT — подстановка body; _createdBy: true — подставить user.userId в createdBy.
*/
const TOOL_EXECUTORS = {
list_buildings: {
method: 'GET',
pathTemplate: '/buildings',
queryParams: null,
bodyMapping: null
},
get_building: {
method: 'GET',
pathTemplate: '/buildings/:id',
queryParams: null,
bodyMapping: null
},
list_districts: {
method: 'GET',
pathTemplate: '/districts',
queryParams: null,
bodyMapping: null
},
list_employees: {
method: 'GET',
pathTemplate: '/employees/list',
queryParams: null,
bodyMapping: null
},
list_payment_invoices: {
method: 'GET',
pathTemplate: '/finance/payment-invoices',
queryParams: { limit: 'limit', page: 'page' },
bodyMapping: null
},
create_payment_invoice: {
method: 'POST',
pathTemplate: '/finance/payment-invoices',
queryParams: null,
bodyMapping: {
_createdBy: true,
contractorName: 'contractorName',
totalAmount: 'totalAmount',
purposeType: 'purposeType',
paymentFormat: 'paymentFormat',
serviceItems: 'serviceItems',
purposeDescription: 'purposeDescription',
contractorInn: 'contractorInn',
notes: 'notes'
}
},
create_application: {
method: 'POST',
pathTemplate: '/applications',
queryParams: null,
bodyMapping: {
address: 'address',
description: 'description',
contactName: 'contactName',
apartment: 'apartment',
deadlineAt: 'deadlineAt'
}
}
};
/**
* Подставить параметры в path (например :id -> args.id)
*/
function buildPath(pathTemplate, args) {
let p = pathTemplate;
const re = /:(\w+)/g;
let m;
while ((m = re.exec(pathTemplate)) !== null) {
const key = m[1];
const val = args && args[key];
if (val === undefined || val === null) {
throw new Error(`Параметр "${key}" обязателен для пути ${pathTemplate}`);
}
p = p.replace(new RegExp(':' + key + '(?=/|$)'), encodeURIComponent(String(val)));
}
return p;
}
/**
* Выполнить инструмент от имени пользователя: внутренний HTTP-запрос к нашему API с JWT.
* @param {string} toolName
* @param {object} args — аргументы от ИИ
* @param {object} user — { userId, employeeId, role } (req.user)
* @param {object} context — { baseUrl, apiPrefix, getTokenForUser }
* @returns {Promise<{ success: boolean, data?: any, error?: string }>}
*/
async function runTool(toolName, args, user, context) {
if (toolName === 'get_capabilities') {
return { success: true, data: { text: CAPABILITIES_TEXT } };
}
const { baseUrl, apiPrefix, getTokenForUser } = context;
const executor = TOOL_EXECUTORS[toolName];
if (!executor) {
return { success: false, error: `Неизвестный инструмент: ${toolName}` };
}
const path = buildPath(executor.pathTemplate, args || {});
let url = `${baseUrl}${apiPrefix}${path}`;
if (executor.queryParams && args) {
const q = [];
for (const [argKey, queryKey] of Object.entries(executor.queryParams)) {
if (args[argKey] !== undefined && args[argKey] !== null) {
q.push(`${encodeURIComponent(queryKey)}=${encodeURIComponent(String(args[argKey]))}`);
}
}
if (q.length) url += '?' + q.join('&');
}
const token = getTokenForUser(user);
const headers = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
};
let body = undefined;
if (executor.bodyMapping && (executor.method === 'POST' || executor.method === 'PUT')) {
body = {};
for (const [argKey, destKey] of Object.entries(executor.bodyMapping)) {
if (argKey === '_createdBy' && destKey === true) {
body.createdBy = user.userId;
continue;
}
if (args && argKey in args) {
body[destKey] = args[argKey];
}
}
}
try {
const res = await axios({
method: executor.method,
url,
headers,
data: body,
timeout: 30000,
validateStatus: () => true
});
if (res.status >= 200 && res.status < 300) {
return { success: true, data: res.data };
}
const errText = res.data && (res.data.error || res.data.message) ? String(res.data.error || res.data.message) : res.statusText;
return { success: false, error: `HTTP ${res.status}: ${errText}` };
} catch (err) {
const msg = err.response ? `HTTP ${err.response.status}` : err.message || 'Ошибка запроса';
return { success: false, error: msg };
}
}
module.exports = {
getToolsSchema: () => TOOLS_FOR_OPENAI,
runTool,
TOOL_EXECUTORS
};