/** * AI zpracovani textu: zarazeni do kategorie. * * Sluzba: https://api.openai.com/v1 (nebezi u nas, adresa je v katalogu) * Endpoint: POST /chat/completions * * Rozdil proti sluzbe OpenAI: tam se posila volny dotaz a vraci se volny text. * Tady je uloha pevna a **vystup taky** - `category` a `confidence`. Prave na * to se da ve strome navazat podminkou, coz u volneho textu nejde. * * Model se bere z konektoru (`ctx.config.model`), aby se nemusel vyplnovat * u kazdeho kroku znovu. */ export const manifest = { id: 'ai-text.classify', name: 'Zařadit do kategorie', description: 'Rozhodne, do které z uvedených kategorií text patří. Vrací kategorii ' + 'a jistotu, takže se za krokem dá větvit podle toho, jak si je model jistý.', inputs: [ { id: 'categories', label: 'Seznam kategorií', type: 'string', required: true, hint: 'Oddělte čárkou, například: porucha, dotaz, reklamace.', }, { id: 'text', label: 'Text', type: 'string', required: true, multiline: true, hint: 'Co se má zařadit. Typicky předmět a tělo ticketu.', }, { id: 'instructions', label: 'Doplňující pravidla', type: 'string', required: false, multiline: true, hint: 'Například: reklamací se rozumí jen stížnost na už dodané zboží.', }, ], outputs: [ { id: 'category', label: 'Kategorie', type: 'string', required: true }, { id: 'confidence', label: 'Jistota', type: 'number', required: true }, { id: 'matched', label: 'Kategorie ze seznamu', type: 'boolean', required: true }, ], timeoutMs: 45000, }; export async function run(inputs, ctx) { const { pick, first, text, num, need } = ctx.util; const categories = inputs.categories .split(',') .map((item) => item.trim()) .filter(Boolean); if (categories.length < 2) ctx.fail('Uveďte aspoň dvě kategorie, jinak není z čeho vybírat.'); const system = 'Jsi klasifikátor. Vrátíš POUZE platný JSON ve tvaru ' + '{"category": "...", "confidence": 0.0}. Pole category musí být přesně jedna ' + `z těchto hodnot: ${categories.join(', ')}. Confidence je číslo od 0 do 1.` + (inputs.instructions ? ` Další pravidla: ${inputs.instructions}` : ''); const { body } = await ctx.http.post('/chat/completions', { model: ctx.config.model || 'gpt-4o-mini', messages: [ { role: 'system', content: system }, { role: 'user', content: inputs.text }, ], response_format: { type: 'json_object' }, }); const content = text(pick(pick(first(pick(body, 'choices')), 'message'), 'content')); if (!content) ctx.fail('Model nevrátil odpověď.', body); let parsed; try { parsed = JSON.parse(content); } catch { ctx.fail(`Model nevrátil platný JSON: ${content}`); } const category = need(text(pick(parsed, 'category')), 'kategorii'); /* * Model obcas vrati kategorii, ktera v seznamu neni, i kdyz ji ma zakazanou. * Neni to duvod krok shodit - vysledek se vrati a `matched` rekne, ze je * mimo seznam. Strom se pak muze rozhodnout, jestli mu verit. */ const matched = categories.some((item) => item.toLowerCase() === category.toLowerCase()); if (!matched) ctx.log(`Model vrátil "${category}", což v seznamu není.`); return { category, confidence: num(pick(parsed, 'confidence'), 0) ?? 0, matched, }; }