"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var AiService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AiService = exports.AI_CATEGORIZATION_MODEL = void 0; const common_1 = require("@nestjs/common"); const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions'; exports.AI_CATEGORIZATION_MODEL = 'mistral-tiny'; const MODEL = exports.AI_CATEGORIZATION_MODEL; let AiService = AiService_1 = class AiService { constructor() { this.logger = new common_1.Logger(AiService_1.name); } async suggestCategory(productName, categories) { const apiKey = process.env.MISTRAL_API_KEY; if (!apiKey) { throw new common_1.ServiceUnavailableException('MISTRAL_API_KEY är inte konfigurerad i miljövariabler'); } const categoryList = categories .map((c) => `[${c.id}] ${c.path}`) .join('\n'); const systemPrompt = `Du är ett kategoriseringssystem för en livsmedelsapp. Din uppgift är att hitta den mest lämpliga kategorin för en produkt. Tillgängliga kategorier (format: [id] Sökväg): ${categoryList} Regler: 1. Välj den mest specifika underkategorin som passar produkten. 2. Om ingen specifik kategori passar, välj en underkategori under "Övrigt" om möjligt. 3. Om ingen underkategori under "Övrigt" passar, välj "Övrigt" (den kategori vars sökväg är exakt "Övrigt"). 4. Du MÅSTE alltid returnera ett svar — aldrig null eller tomt. 5. Svara ENDAST med giltig JSON i detta format: { "categoryId": , "confidence": "high" | "medium" | "low" } - "high": uppenbart matchande kategori - "medium": trolig matchning - "low": osäker, används fallback (Övrigt eller underkategori till Övrigt)`; const userPrompt = `Produkt: "${productName}"`; let raw = ''; const MAX_RETRIES = 3; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = await fetch(MISTRAL_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model: MODEL, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], max_tokens: 100, temperature: 0.1, response_format: { type: 'json_object' }, }), }); if (response.status === 503 || response.status === 429) { const err = await response.text(); this.logger.warn(`Mistral API ${response.status} (försök ${attempt}/${MAX_RETRIES}): ${err}`); if (attempt < MAX_RETRIES) { await new Promise((r) => setTimeout(r, attempt * 1500)); continue; } throw new common_1.ServiceUnavailableException('AI-tjänsten är tillfälligt otillgänglig, försök igen'); } if (!response.ok) { const err = await response.text(); this.logger.error(`Mistral API-fel: ${response.status} ${err}`); throw new common_1.ServiceUnavailableException('AI-tjänsten svarade inte korrekt'); } const data = await response.json(); raw = data.choices?.[0]?.message?.content ?? ''; break; } catch (err) { if (err instanceof common_1.ServiceUnavailableException) throw err; this.logger.error(`Mistral fetch-fel (försök ${attempt}/${MAX_RETRIES}): ${String(err)}`); if (attempt < MAX_RETRIES) { await new Promise((r) => setTimeout(r, attempt * 1500)); continue; } throw new common_1.ServiceUnavailableException('Kunde inte nå AI-tjänsten'); } } let parsed; try { parsed = JSON.parse(raw); } catch { this.logger.warn(`AI returnerade ogiltig JSON: ${raw}`); return this.fallbackToOvrigt(categories); } const validId = typeof parsed.categoryId === 'number'; const matchedCategory = validId ? categories.find((c) => c.id === parsed.categoryId) : null; if (!matchedCategory) { this.logger.warn(`AI returnerade okänt categoryId ${parsed.categoryId}, använder fallback`); return this.fallbackToOvrigt(categories); } const confidence = ['high', 'medium', 'low'].includes(parsed.confidence) ? parsed.confidence : 'medium'; if (confidence === 'low') { const l1Name = matchedCategory.path.split(' > ')[0]; const l1 = categories.find((c) => c.path === l1Name); if (l1 && l1.id !== matchedCategory.id) { this.logger.log(`AI-guardrail: ${confidence} konfidenspoäng → remappar "${matchedCategory.path}" → L1 "${l1.path}"`); return { categoryId: l1.id, categoryName: l1.name, path: l1.path, confidence, usedFallback: true, }; } } return { categoryId: matchedCategory.id, categoryName: matchedCategory.name, path: matchedCategory.path, confidence, usedFallback: confidence === 'low', }; } fallbackToOvrigt(categories) { const ovrigt = categories.find((c) => c.path === 'Övrigt'); if (!ovrigt) { const first = categories[0]; return { categoryId: first.id, categoryName: first.name, path: first.path, confidence: 'low', usedFallback: true }; } return { categoryId: ovrigt.id, categoryName: ovrigt.name, path: ovrigt.path, confidence: 'low', usedFallback: true, }; } }; exports.AiService = AiService; exports.AiService = AiService = AiService_1 = __decorate([ (0, common_1.Injectable)() ], AiService); //# sourceMappingURL=ai.service.js.map