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