Files
mkd/backend/aiToolsRegistry.js

313 lines
14 KiB
JavaScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
/**
* Реестр инструментов ИИ-чата: описание для модели (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
};