"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ParserService = void 0; class ParserService { constructor(prisma, aiService) { this.prisma = prisma; this.aiService = aiService; } /** Режим обсуждения сметы: свободный диалог о прикреплённой смете */ async processEstimateDiscussion(content, history, estimateId) { const estimate = await this.prisma.estimate.findUnique({ where: { id: estimateId }, include: { direction: true, items: { orderBy: { orderNumber: 'asc' } }, totals: { orderBy: { orderNumber: 'asc' } }, }, }); if (!estimate) return null; const estimateJson = { objectName: estimate.objectName, customer: estimate.customer, executor: estimate.executor || undefined, direction: estimate.direction?.code, items: estimate.items.map(i => ({ workName: i.workName, quantity: Number(i.quantity), unit: i.unit || undefined, basePrice: Number(i.basePrice), totalPrice: Number(i.totalPrice), })), totals: estimate.totals?.map(t => ({ label: t.label, resultValue: Number(t.resultValue), })), }; const historyForAi = history.map(m => ({ role: m.role, content: m.content, })); const message = await this.aiService.discussEstimate(content, historyForAi, estimateJson); return { message, needsClarification: false, }; } /** Берём последние извлечённые данные из истории (ответ ассистента с metadata) */ getPreviousExtractedData(history) { for (let i = history.length - 1; i >= 0; i--) { const msg = history[i]; if (msg.role === 'assistant' && msg.metadata && typeof msg.metadata === 'object') { const data = msg.metadata; if (data.direction || data.customer || data.objectName || (data.works && data.works.length > 0)) { return data; } } } return null; } isPlaceholder(val) { if (!val || typeof val !== 'string') return true; const t = val.trim().toLowerCase(); return ParserService.PLACEHOLDER_VALUES.some(p => t === p.toLowerCase() || t === ''); } /** Объединяем новые данные с предыдущими: не затираем поля, которые пользователь уже указал */ mergeExtractedData(previous, fresh) { const take = (freshVal, prevVal) => { if (freshVal?.trim() && !this.isPlaceholder(freshVal)) return freshVal.trim(); if (prevVal?.trim()) return prevVal; return undefined; }; return { direction: take(fresh.direction, previous?.direction), customer: take(fresh.customer, previous?.customer), objectName: take(fresh.objectName, previous?.objectName), executor: take(fresh.executor, previous?.executor), works: (fresh.works && fresh.works.length > 0) ? fresh.works : (previous?.works ?? fresh.works), vatIncluded: fresh.vatIncluded ?? previous?.vatIncluded, }; } async processMessage(content, history, estimateId) { // Режим обсуждения сметы: когда есть прикреплённая смета if (estimateId) { const discussionResult = await this.processEstimateDiscussion(content, history, estimateId); if (discussionResult) return discussionResult; } const previousData = this.getPreviousExtractedData(history); let estimateContext; if (estimateId) { const estimate = await this.prisma.estimate.findUnique({ where: { id: estimateId }, include: { direction: true, items: { orderBy: { orderNumber: 'asc' } }, }, }); if (estimate) { estimateContext = { objectName: estimate.objectName, customer: estimate.customer, executor: estimate.executor || undefined, direction: estimate.direction?.code, items: estimate.items.map(i => ({ workName: i.workName, quantity: Number(i.quantity), unit: i.unit || undefined, })), }; } } const freshData = await this.aiService.extractEstimateData(content, previousData ?? undefined, estimateContext); if (freshData.error) { return { message: 'Не удалось обработать сообщение. Пожалуйста, опишите подробнее: какой объект, какие работы нужно выполнить?', needsClarification: true, clarificationQuestions: [ 'Какое направление изысканий? (геодезия, геология, экология, гидрометеорология)', 'Кто заказчик?', 'Как называется объект?', 'Какие работы и в каком объёме?', ], }; } const extractedData = this.mergeExtractedData(previousData, freshData); // Check for missing fields const missingFields = []; const questions = []; if (!extractedData.direction) { missingFields.push('direction'); questions.push('Укажите направление изысканий: геодезия, геология, экология или гидрометеорология?'); } if (!extractedData.customer) { missingFields.push('customer'); questions.push('Кто заказчик работ?'); } if (!extractedData.objectName) { missingFields.push('objectName'); questions.push('Как называется объект? (полное наименование объекта изысканий/строительства)'); } if (!extractedData.works || extractedData.works.length === 0) { missingFields.push('works'); questions.push('Какие работы нужно выполнить и в каком объёме?'); } // If we have enough data, try to match works with price items (СБЦ) if (extractedData.works && extractedData.works.length > 0) { await this.matchWorksWithPriceItems(extractedData); } // Build response message let message = ''; if (extractedData.direction || extractedData.customer || extractedData.objectName) { message += 'Извлечённые данные:\n'; if (extractedData.direction) { const directionNames = { geodesy: 'Инженерно-геодезические изыскания', geology: 'Инженерно-геологические изыскания', ecology: 'Инженерно-экологические изыскания', hydrology: 'Инженерно-гидрометеорологические изыскания', }; message += `- Направление: ${directionNames[extractedData.direction] || extractedData.direction}\n`; } if (extractedData.customer) { message += `- Заказчик: ${extractedData.customer}\n`; } if (extractedData.objectName) { message += `- Объект: ${extractedData.objectName}\n`; } if (extractedData.works && extractedData.works.length > 0) { message += `- Работы (${extractedData.works.length}):\n`; for (const work of extractedData.works) { message += ` • ${work.name}: ${work.volume} ${work.unit}`; if (work.justification) { message += ` (${work.justification})`; } message += '\n'; } } } if (missingFields.length > 0) { message += '\nДля продолжения уточните:\n'; questions.forEach((q, i) => { message += `${i + 1}. ${q}\n`; }); return { message, extractedData, needsClarification: true, clarificationQuestions: questions, }; } // All data is present message += '\nВсе данные получены. Смета готова к формированию. Создать смету?'; return { message, extractedData, needsClarification: false, }; } async matchWorksWithPriceItems(data) { if (!data.works || !data.direction) return; // Get relevant price book const priceBookCode = this.getRelevantPriceBookCode(data.direction); const priceBook = await this.prisma.priceBook.findFirst({ where: { code: priceBookCode }, }); if (!priceBook) return; // Get all price items for this book const priceItems = await this.prisma.priceItem.findMany({ where: { priceBookId: priceBook.id }, include: { priceTable: true }, }); // Match each work for (const work of data.works) { const matches = await this.aiService.findPriceItems(work.name, priceItems); if (matches.length > 0) { const bestMatch = matches[0]; work.priceItemId = bestMatch.id; work.justification = `${bestMatch.priceTable.name} ${bestMatch.paragraph}`; } } } getRelevantPriceBookCode(direction) { const mapping = { geodesy: 'SBC-GEODESY-2004', geology: 'SBC-GEOLOGY-1999', ecology: 'SBC-GEOLOGY-1999', hydrology: 'SBC-HYDROLOGY-2001', }; return mapping[direction] || 'SBC-GEODESY-2004'; } async createEstimateFromParsedData(data) { if (!data.direction || !data.customer || !data.objectName) { throw new Error('Missing required fields'); } // Get direction const direction = await this.prisma.surveyDirection.findUnique({ where: { code: data.direction }, }); if (!direction) { throw new Error('Invalid direction'); } // Get default settings const executorSetting = await this.prisma.setting.findUnique({ where: { key: 'default_executor' }, }); const vatSetting = await this.prisma.setting.findUnique({ where: { key: 'default_vat_rate' }, }); // Generate estimate number const count = await this.prisma.estimate.count(); // Create estimate const estimate = await this.prisma.estimate.create({ data: { number: `${count + 1}`, directionId: direction.id, objectName: data.objectName, customer: data.customer, executor: data.executor || executorSetting?.value || 'Не указан', vatRate: vatSetting ? parseFloat(vatSetting.value) : 20, status: 'draft', }, }); // Add works as estimate items; наименование работ — из справочника СБЦ (workType), если есть совпадение if (data.works && data.works.length > 0) { for (let i = 0; i < data.works.length; i++) { const work = data.works[i]; let basePrice = 0; let workName = work.name; let unit = work.unit; if (work.priceItemId) { const priceItem = await this.prisma.priceItem.findUnique({ where: { id: work.priceItemId }, include: { priceTable: true }, }); if (priceItem) { if (priceItem.priceSimple) basePrice = Number(priceItem.priceSimple); workName = priceItem.workType; if (priceItem.priceTable?.unit) unit = priceItem.priceTable.unit; } } await this.prisma.estimateItem.create({ data: { estimateId: estimate.id, orderNumber: i + 1, sectionType: 'field', priceItemId: work.priceItemId || null, workName, justification: work.justification || null, basePrice, quantity: work.volume, unit, totalPrice: basePrice * work.volume, }, }); } } return estimate; } } exports.ParserService = ParserService; /** Значения-заполнители: не перезаписываем ими ранее указанные данные */ ParserService.PLACEHOLDER_VALUES = [ 'не указан', 'не указано', 'не указана', 'не указаны', '—', '–', '-', 'н/д', 'н.д.', 'не задан', 'отсутствует', '', ]; //# sourceMappingURL=parser.service.js.map