Files
geo/backend/dist/services/ai.service.js

205 lines
11 KiB
JavaScript
Raw Normal View History

2026-02-04 00:11:19 +05:00
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIService = void 0;
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
class AIService {
constructor() {
this.provider = process.env.AI_PROVIDER || 'iieasy';
this.iieasyUrl = process.env.IIEASY_API_URL || 'https://ai.iieasy.ru/v1';
this.iieasyKey = process.env.IIEASY_API_KEY || '';
this.iieasyModel = process.env.IIEASY_MODEL || 'google/gemma-3n-e4b';
this.lmstudioUrl = process.env.LMSTUDIO_API_URL || 'http://localhost:1234/v1';
this.lmstudioModel = process.env.LMSTUDIO_MODEL || 'local-model';
}
setProvider(provider) {
this.provider = provider;
}
async chat(messages, systemPrompt) {
const allMessages = [];
if (systemPrompt) {
allMessages.push({ role: 'system', content: systemPrompt });
}
allMessages.push(...messages);
if (this.provider === 'lmstudio') {
return this.chatLMStudio(allMessages);
}
else {
return this.chatIIEasy(allMessages);
}
}
async chatIIEasy(messages) {
try {
const response = await fetch(`${this.iieasyUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.iieasyKey}`,
},
body: JSON.stringify({
model: this.iieasyModel,
messages,
temperature: 0.7,
max_tokens: 4096,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`IIEasy API error: ${response.status} - ${error}`);
}
const data = await response.json();
return {
content: data.choices[0]?.message?.content || '',
usage: data.usage,
};
}
catch (error) {
console.error('IIEasy API error:', error);
throw new Error(`AI service error: ${error.message}`);
}
}
async chatLMStudio(messages) {
try {
const response = await fetch(`${this.lmstudioUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.lmstudioModel,
messages,
temperature: 0.7,
max_tokens: 4096,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`LM Studio API error: ${response.status} - ${error}`);
}
const data = await response.json();
return {
content: data.choices[0]?.message?.content || '',
usage: data.usage,
};
}
catch (error) {
console.error('LM Studio API error:', error);
throw new Error(`AI service error: ${error.message}`);
}
}
async extractEstimateData(text, previousData, estimateContext) {
const prevHints = [];
if (previousData?.customer?.trim())
prevHints.push(`Заказчик: "${previousData.customer}" — сохрани в customer, если в тексте не указан другой.`);
if (previousData?.direction?.trim())
prevHints.push(`Направление: "${previousData.direction}" — сохрани в direction, если пользователь не указал другое.`);
if (previousData?.objectName?.trim())
prevHints.push(`Объект: "${previousData.objectName}" — сохрани в objectName, если пользователь не указал другой.`);
if (previousData?.executor?.trim())
prevHints.push(`Исполнитель: "${previousData.executor}" — сохрани в executor, если не указан другой.`);
if (previousData?.works?.length)
prevHints.push(`Работы уже указаны — добавляй только новые, либо объединяй с ними.`);
const previousDataHint = prevHints.length
? `\nРанее в беседе указано:\n${prevHints.join('\n')}\nВ этих полях НЕ пиши "не указан" — используй ранее указанное значение или опусти поле.\n`
: '';
const estimateContextHint = estimateContext
? `\nТекущая смета (контекст беседы):\n- Объект: ${estimateContext.objectName}\n- Заказчик: ${estimateContext.customer}\n${estimateContext.executor ? `- Исполнитель: ${estimateContext.executor}\n` : ''}${estimateContext.direction ? `- Направление: ${estimateContext.direction}\n` : ''}${estimateContext.items && estimateContext.items.length > 0 ? `- Позиции сметы: ${estimateContext.items.map(i => `${i.workName} (${i.quantity} ${i.unit || 'шт.'})`).join(', ')}\n` : ''}\nОтвечай в контексте этой сметы. Пользователь может просить добавить работы, уточнить данные или задавать вопросы о смете.\n`
: '';
const systemPrompt = `Ты - ассистент для составления смет на изыскательские работы.
Проанализируй текст и извлеки следующую информацию в JSON:
- direction: направление изысканий ОДНО из: geodesy, geology, ecology, hydrology (не перечень работ).
- customer: заказчик (организация или ФИО).
- objectName: полное наименование ОБЪЕКТА изысканий/строительства это название объекта, НЕ перечень работ.
Примеры правильного objectName: «АО «Святогор». Месторождение «Волковское». Третья очередь. Комплекс объектов инфраструктуры обогатительной фабрики и открытого рудника», «Строительство школы в г. Москва».
НИКОГДА не подставляй в objectName: "работы", "перечень работ", названия видов работ (топосъёмка, нивелирование и т.п.) это только для поля works.
- works: список работ с объёмами (name краткое описание работы для подбора по СБЦ, volume число, unit единица: га, км, шт. и т.д.).
${previousDataHint}
${estimateContextHint}
Отвечай ТОЛЬКО в формате JSON, без дополнительного текста.
Пример ответа:
{
"direction": "geodesy",
"customer": "ООО Компания",
"objectName": "АО «Святогор». Месторождение «Волковское». Третья очередь. Комплекс объектов инфраструктуры обогатительной фабрики и открытого рудника",
"works": [
{"name": "Топографическая съемка", "volume": 10, "unit": "га"},
{"name": "Нивелирование", "volume": 5, "unit": "км"}
]
}`;
const response = await this.chat([{ role: 'user', content: text }], systemPrompt);
try {
// Try to parse JSON from response
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('No JSON found in response');
}
catch (error) {
console.error('Failed to parse AI response:', response.content);
return {
error: true,
rawResponse: response.content,
};
}
}
async discussEstimate(userMessage, history, estimateJson) {
const itemsStr = estimateJson.items && estimateJson.items.length > 0
? estimateJson.items.map((i, idx) => `${idx + 1}. ${i.workName}: ${i.quantity} ${i.unit || 'шт.'}${i.totalPrice != null ? i.totalPrice + ' руб.' : ''}`).join('\n')
: 'Нет позиций';
const totalsStr = estimateJson.totals && estimateJson.totals.length > 0
? estimateJson.totals.map(t => `${t.label}: ${t.resultValue} руб.`).join('\n')
: '';
const systemPrompt = `Ты — эксперт-сметчик по изыскательским работам. Пользователь обсуждает смету. Отвечай на русском.
Текущая смета:
- Объект: ${estimateJson.objectName}
- Заказчик: ${estimateJson.customer}
${estimateJson.executor ? `- Исполнитель: ${estimateJson.executor}\n` : ''}${estimateJson.direction ? `- Направление: ${estimateJson.direction}\n` : ''}
Позиции:
${itemsStr}
${totalsStr ? `\nИтоги:\n${totalsStr}` : ''}
Помогай с рекомендациями, оптимизацией, корректировкой позиций и объёмов. Отвечай кратко и по делу.`;
const messages = [
...history.slice(-10).map(m => ({
role: m.role,
content: m.content,
})),
{ role: 'user', content: userMessage },
].filter(m => m.role !== 'system');
const response = await this.chat(messages, systemPrompt);
return response.content;
}
async findPriceItems(workDescription, priceItems) {
const systemPrompt = `Ты - эксперт по сметному делу в изысканиях.
Тебе даны: описание работы и список позиций из справочника базовых цен.
Найди наиболее подходящие позиции для данной работы.
Отвечай ТОЛЬКО в формате JSON - массив индексов подходящих позиций.
Пример: [0, 2, 5]
Если ничего не подходит, верни пустой массив: []`;
const itemsList = priceItems.map((item, idx) => `${idx}. ${item.paragraph}: ${item.workType}`).join('\n');
const response = await this.chat([{ role: 'user', content: `Работа: ${workDescription}\n\nПозиции справочника:\n${itemsList}` }], systemPrompt);
try {
const jsonMatch = response.content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const indices = JSON.parse(jsonMatch[0]);
return indices.map((idx) => priceItems[idx]).filter(Boolean);
}
return [];
}
catch (error) {
return [];
}
}
}
exports.AIService = AIService;
//# sourceMappingURL=ai.service.js.map