205 lines
11 KiB
JavaScript
Executable File
205 lines
11 KiB
JavaScript
Executable File
"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
|