AI zpracovani textu funguje: klic, adresa a tri skripty

Sluzba byla v katalogu bez pristupovych udaju a s appId 'ai-text', tedy
mirila na aplikaci, ktera neexistuje. Konektor nemel co vyplnit a krok nemel
co vykonat.

- sluzba stoji na api.openai.com/v1 stejne jako OpenAI, overeni GET /models
- model se bere z konektoru (ctx.config.model), ne z kazdeho kroku zvlast
- skripty ai-text.classify, ai-text.extract, ai-text.generate

Proc to neni jedna sluzba s OpenAI: tam se posila volny dotaz a vraci volny
text. Tady je uloha pevna a vystup taky - category a confidence, objekt
s popsanymi poli, text s poctem slov. Na to jde ve strome navazat podminkou,
na volny text ne.

Zjisteno u toho a zapsane do 21-realne-sluzby.md: proxy vraci na neznamou
aplikaci 200 s prazdnym telem, takze overeni konektoru u sluzby bez
verifyPath projde i tam, kde nic nebezi. Zatim neopraveno.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-28 07:32:18 +02:00
co-authored by Claude Opus 5
parent d71c111cf3
commit aa0a783095
6 changed files with 461 additions and 24 deletions
+106
View File
@@ -0,0 +1,106 @@
/**
* 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,
};
}