"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