Files
geo/backend/dist/services/parser.service.js
2026-02-04 00:11:19 +05:00

314 lines
14 KiB
JavaScript
Executable File
Raw Permalink 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.
"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