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