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

205 lines
11 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";
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