diff --git a/documentation/13-transformace-dat.md b/documentation/13-transformace-dat.md index 4fa5603..34a4bd9 100644 --- a/documentation/13-transformace-dat.md +++ b/documentation/13-transformace-dat.md @@ -188,6 +188,86 @@ nedodelku, viz [05-dashboard-a-builder.md](05-dashboard-a-builder.md). Neznamy prevod je pri behu chyba, ne preskoceni. Tise ho ignorovat by znamenalo odeslat neprevedena data a divit se az u odberatele. +## Vlastni kategorie, ne polozka v Obecnem + +Transformace maji v katalogu **svou kategorii** (`transformace`). Driv byly mezi +obecnymi sluzbami vedle webhooku a pauzy, coz prestalo davat smysl ve chvili, +kdy jich zacalo byt vic nez tri: clovek je hleda jako skupinu, ne mezi +spoustecemi. + +## Navrh cele skupiny + +Co uz existuje a co dava smysl doplnit. Neni to seznam vseho, co jde vymyslet, +je to seznam toho, co v automatizacich opravdu chybi. + +### Formaty + +| Operace | Co dela | Stav | +| ---------- | --------------------------------------------- | ------ | +| `to-json` | poskladat objekt ze sablony | hotovo | +| `to-xml` | zapsat objekt jako XML vcetne escapovani | hotovo | +| `to-csv` | seznam na CSV: oddelovac, hlavicka, uvozovani | navrh | +| `from-csv` | CSV na seznam objektu | navrh | +| `from-xml` | XML na objekt | navrh | +| `to-text` | textova sablona pro e-mail nebo poznamku | navrh | + +Cizi systemy si XML a CSV rikaji porad casto: ISDOC, EDI, banky, e-shopy, +statni sluzby. Bez techhle kroku se to sestavuje retezcem v HTTP kroku +a na escapovani se zapomene pokazde. + +### Pole a hodnoty + +| Operace | Co dela | Stav | +| ------------ | --------------------------------- | ------ | +| `map-fields` | premapovat pole a prevest hodnoty | hotovo | +| `merge` | spojit dva objekty do jednoho | navrh | + +`merge` chybi pokazde, kdyz krok dohleda zakaznika v CRM a data se maji spojit +s tim, co prislo webhookem. Dnes se to obchazi sablonou JSON, ktera musi vypsat +obe strany rucne. + +### Seznamy + +| Operace | Co dela | Stav | +| ------------- | ------------------------------------------------------- | ----------------------- | +| `filter` | nechat jen prvky, ktere splnuji podminku | navrh | +| `sort` | seradit podle pole | navrh | +| `group-by` | seskupit podle pole | navrh | +| `aggregate` | soucet, pocet, prumer, min, max, volitelne po skupinach | navrh | +| `deduplicate` | odstranit duplicity podle klice | v katalogu, bez skriptu | +| `join` | spojit dva seznamy podle spolecneho klice | navrh | +| `flatten` | rozbalit zanoreni a seznamy seznamu | navrh | + +Tohle je nejvetsi dira. Objednavka ma polozky, faktura ma radky, report ma +zaznamy - a dnes s nimi krok neumi udelat nic jineho nez predat je dal celé. + +### Text + +| Operace | Co dela | Stav | +| --------- | ------------------------------------ | ----- | +| `extract` | vytahnout hodnotu regularnim vyrazem | navrh | + +Variabilni symbol z predmetu e-mailu, cislo objednavky ze zpravy. Dnes na to +musi jit jazykovy model, coz je za neco, co zvladne jeden vyraz, drahé a pomalé. + +### Kontrola + +| Operace | Co dela | Stav | +| ---------- | ---------------------------------------------- | ----- | +| `validate` | overit povinna pole a typy, vratit seznam chyb | navrh | + +Nejdulezitejsi z celeho seznamu. Bez nej se vadna data posilaji cizi sluzbe +a chyba se pozna az z jeji odpovedi - tedy pozde, po zapisu, a s hlaskou, ktera +mluvi o jejich schematu, ne o nasich datech. + +### Co v seznamu zamerne neni + +- **Prevody jednotlivych hodnot** (datum, cislo, velikost pismen). Uz jsou + v `map-fields` jako prevody a druhy zpusob, jak udelat totez, by jen mnozil + otazku, ktery pouzit. +- **Vlastni skript.** Uz existuje jako `transform.custom` a je to zamerne + posledni moznost, ne prvni. + ## Kde to je ``` diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index 2429243..ff4e8c6 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,31 @@ Nejnovejsi nahore. +## 2026-08-28 - transformace maji svou kategorii, pribylo XML + +### Odstraneno + +- **Sluzba AI zpracovani textu.** Delala totez co OpenAI, jen jinymi slovy. + Dva zpusoby, jak udelat jednu vec, znamenaji jen otazku, ktery pouzit. + Ukazkove stromy a stopy, ktere na ni odkazovaly, ted miri na `openai.chat`. + +### Pridano + +- **Kategorie Transformace dat.** Driv byly mezi obecnymi sluzbami vedle + webhooku a pauzy. Ve chvili, kdy jich je vic nez tri, je clovek hleda jako + skupinu, ne mezi spoustecemi. +- **Skript `transform.to-xml`.** Zapise objekt jako XML vcetne escapovani. + Cizi systemy si XML porad rikaji casto (ISDOC, EDI, banky, statni sprava) + a bez tohohle se sestavuje retezcem v HTTP kroku, kde se na escapovani + zapomene pokazde. + + Klice, ktere nejsou platny nazev prvku, se prejmenuji a rekne se to nahlas. + Klic zacinajici cislici dostane podtrzitko misto odriznuti - `2024` a `2025` + by jinak byly totez a v dokumentu by se srazily. +- **Navrh cele skupiny transformaci** v [13-transformace-dat.md](13-transformace-dat.md): + formaty, pole a hodnoty, seznamy, text a kontrola. Vcetne toho, co v seznamu + zamerne neni a proc. + ## 2026-08-28 - AI zpracovani textu funguje Sluzba byla v katalogu bez pristupovych udaju a s `appId: 'ai-text'`, tedy diff --git a/scripts/ai-text.classify.js b/scripts/ai-text.classify.js deleted file mode 100644 index eb1f7af..0000000 --- a/scripts/ai-text.classify.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 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, - }; -} diff --git a/scripts/ai-text.extract.js b/scripts/ai-text.extract.js deleted file mode 100644 index 48a52e9..0000000 --- a/scripts/ai-text.extract.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * AI zpracovani textu: vytazeni udaju. - * - * Sluzba: https://api.openai.com/v1 - * Endpoint: POST /chat/completions - * - * Vzor **vystupu, ktery si urcuje uzivatel**. Pole se popisou v kroku stromu - * a model je vrati jako objekt. Manifest je predem znat nemuze, proto se vraci - * jeden parametr typu `object` a vedle nej priznak, co se opravdu naslo. - * - * Nenalezeny udaj je `null`, ne vymysleny. Model, ktery hada, je horsi nez - * model, ktery rekne "nevim" - vymyslene IC se poznat neda. - */ - -export const manifest = { - id: 'ai-text.extract', - name: 'Vytáhnout údaje', - description: - 'Z volného textu získá strukturovaná data podle polí, která popíšete. ' + - 'Co v textu není, vrátí prázdné, nedomýšlí si.', - - inputs: [ - { - id: 'fields', - label: 'Pole k vytažení', - type: 'string', - required: true, - multiline: true, - hint: - 'Jedno pole na řádek ve tvaru nazev: popis. Například:\n' + - 'cisloObjednavky: číslo objednávky, obvykle ve tvaru OBJ-1234\n' + - 'castka: celková částka v korunách jako číslo', - }, - { - id: 'text', - label: 'Text', - type: 'string', - required: true, - multiline: true, - }, - ], - - outputs: [ - { id: 'data', label: 'Vytažené údaje', type: 'object', required: true }, - { id: 'found', label: 'Kolik polí se našlo', type: 'number', required: true }, - { id: 'missing', label: 'Co se nenašlo', type: 'string', required: false }, - { id: 'complete', label: 'Našlo se všechno', type: 'boolean', required: true }, - ], - - timeoutMs: 45000, -}; - -export async function run(inputs, ctx) { - const { pick, first, text } = ctx.util; - - const fields = inputs.fields - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const at = line.indexOf(':'); - return at === -1 - ? { name: line, description: '' } - : { name: line.slice(0, at).trim(), description: line.slice(at + 1).trim() }; - }) - .filter((field) => field.name !== ''); - - if (fields.length === 0) ctx.fail('Nepopsali jste žádné pole, takže není co vytáhnout.'); - - const wanted = fields.map((f) => `- ${f.name}${f.description ? `: ${f.description}` : ''}`); - - const system = - 'Vytáhni z textu uvedená pole a vrať POUZE platný JSON objekt s přesně těmito klíči:\n' + - `${wanted.join('\n')}\n` + - 'Co v textu není, vrať jako null. Nic si nedomýšlej a nic nedopočítávej.'; - - 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}`); - } - - // Vysledek se sklada podle **popsanych poli**, ne podle toho, co model poslal. - // Kdyby si pridal klic navic, strom by o nem nevedel a jen by mu zabiral misto. - const data = {}; - const missing = []; - for (const field of fields) { - const value = parsed[field.name]; - data[field.name] = value === undefined ? null : value; - if (value === undefined || value === null || value === '') missing.push(field.name); - } - - if (missing.length > 0) ctx.log(`Nenašlo se: ${missing.join(', ')}.`); - - return { - data, - found: fields.length - missing.length, - missing: missing.length > 0 ? missing.join(', ') : null, - complete: missing.length === 0, - }; -} diff --git a/scripts/ai-text.generate.js b/scripts/ai-text.generate.js deleted file mode 100644 index 3ff9c2a..0000000 --- a/scripts/ai-text.generate.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * AI zpracovani textu: napsani textu podle instrukce. - * - * Sluzba: https://api.openai.com/v1 - * Endpoint: POST /chat/completions - * - * Typicky se z toho stava odpoved zakaznikovi nebo shrnuti do ticketu, takze - * krok umi omezit delku a jazyk. Bez toho model odpovi, jak se mu zachce, - * a odpoved se do e-mailu nebo do komentare nevejde. - */ - -export const manifest = { - id: 'ai-text.generate', - name: 'Vygenerovat text', - description: - 'Napíše text podle instrukce a vstupních dat. Hodí se na odpověď ' + - 'zákazníkovi, shrnutí do ticketu nebo popis incidentu.', - - inputs: [ - { - id: 'instruction', - label: 'Co má napsat', - type: 'string', - required: true, - multiline: true, - hint: 'Například: Napiš zdvořilou odpověď zákazníkovi, že reklamaci přebíráme.', - }, - { - id: 'input', - label: 'Vstupní data', - type: 'string', - required: false, - multiline: true, - hint: 'Text, ze kterého má vycházet. Typicky obsah ticketu.', - }, - { - id: 'tone', - label: 'Tón', - type: 'string', - required: false, - options: [ - { value: 'vecny', label: 'Věcný' }, - { value: 'vstricny', label: 'Vstřícný' }, - { value: 'strucny', label: 'Stručný' }, - { value: 'omluvny', label: 'Omluvný' }, - ], - }, - { - id: 'language', - label: 'Jazyk', - type: 'string', - required: false, - default: 'čeština', - hint: 'Bez vyplnění čeština.', - }, - { - id: 'maxWords', - label: 'Nejvýše kolik slov', - type: 'number', - required: false, - hint: 'Bez vyplnění rozhoduje model. Do e-mailu se hodí kolem 120.', - }, - ], - - outputs: [ - { id: 'text', label: 'Text', type: 'string', required: true }, - { id: 'words', label: 'Počet slov', type: 'number', required: true }, - ], - - timeoutMs: 60000, -}; - -const tones = { - vecny: 'Piš věcně a bez zdobení.', - vstricny: 'Piš vstřícně a lidsky, ale ne přehnaně.', - strucny: 'Piš co nejstručněji, ideálně pár vět.', - omluvny: 'Omluv se za potíže, ale neslibuj nic, co není v zadání.', -}; - -export async function run(inputs, ctx) { - const { pick, first, text, need } = ctx.util; - - const rules = [ - `Piš v jazyce: ${inputs.language}.`, - inputs.tone ? tones[inputs.tone] : '', - inputs.maxWords !== null ? `Vejdi se do ${inputs.maxWords} slov.` : '', - // Bez tohohle model rad pripoji "Jiste, tady je vas text:" a to jde - // rovnou zakaznikovi do e-mailu. - 'Vrať pouze výsledný text, žádný úvod ani komentář k zadání.', - ].filter(Boolean); - - const { body } = await ctx.http.post('/chat/completions', { - model: ctx.config.model || 'gpt-4o-mini', - messages: [ - { role: 'system', content: rules.join(' ') }, - { - role: 'user', - content: inputs.input - ? `${inputs.instruction}\n\nVstupní data:\n${inputs.input}` - : inputs.instruction, - }, - ], - }); - - const content = need( - text(pick(pick(first(pick(body, 'choices')), 'message'), 'content')), - 'vygenerovaný text', - ); - - return { - text: content, - words: content.split(/\s+/).filter(Boolean).length, - }; -} diff --git a/scripts/transform.to-xml.js b/scripts/transform.to-xml.js new file mode 100644 index 0000000..120388e --- /dev/null +++ b/scripts/transform.to-xml.js @@ -0,0 +1,166 @@ +/** + * Transformace dat: prevod na XML. + * + * Cizi systemy si XML porad rikaji casto - ISDOC, EDI, banky, statni sluzby. + * Bez tohohle by se muselo sestavovat retezcem v HTTP kroku, a tam se na + * escapovani zapomene pokazde. + * + * Vstupem je **objekt nebo seznam**, ne sablona. Tvar se resi krokem pred tim + * (`transform.map-fields` nebo `transform.to-json`), tady uz jde jen o zapis. + * Jedna vec na krok: jinak by tenhle skript musel umet i premapovani a byly by + * dva zpusoby, jak udelat totez. + */ + +export const manifest = { + id: 'transform.to-xml', + name: 'Převést na XML', + description: + 'Zapíše objekt nebo seznam jako XML. Tvar dat se řeší předchozím krokem, ' + + 'tady jde jen o zápis včetně escapování.', + + inputs: [ + { + id: 'source', + label: 'Zdrojová data', + type: 'object', + required: true, + hint: 'Objekt z předchozího kroku. Seznam zabalte do objektu, například {"polozky": [...]}.', + }, + { + id: 'root', + label: 'Kořenový prvek', + type: 'string', + required: false, + default: 'data', + pattern: '^[A-Za-z_][A-Za-z0-9._-]*$', + }, + { + id: 'itemElement', + label: 'Název prvku pro položky seznamu', + type: 'string', + required: false, + default: 'item', + hint: 'XML nezná pole, takže každá položka potřebuje svůj prvek.', + pattern: '^[A-Za-z_][A-Za-z0-9._-]*$', + }, + { + id: 'declaration', + label: 'Přidat hlavičku XML', + type: 'boolean', + required: false, + default: true, + hint: 'Řádek s verzí a kódováním. Některé služby ho vyžadují.', + }, + { + id: 'indent', + label: 'Odsazovat', + type: 'boolean', + required: false, + default: true, + hint: 'Vypněte u velkých dávek, odsazení jen zvětšuje přenos.', + }, + { + id: 'skipEmpty', + label: 'Vynechat prázdné hodnoty', + type: 'boolean', + required: false, + default: false, + hint: 'Zapnuté nezapíše pole, která jsou null nebo prázdná.', + }, + ], + + outputs: [ + { id: 'xml', label: 'XML', type: 'string', required: true }, + { id: 'elements', label: 'Počet zapsaných prvků', type: 'number', required: true }, + { id: 'renamed', label: 'Přejmenované klíče', type: 'string', required: false }, + ], +}; + +/** Znaky, ktere v textu meni vyznam. Bez nahrady vznikne nevalidni XML. */ +function escapeText(value) { + return String(value).replace(/&/g, '&').replace(//g, '>'); +} + +/** + * Nazev prvku podle pravidel XML. + * + * Klice z ciziho JSONu casto zacinaji cislem nebo maji mezeru (`order number`, + * `2024`). Takovy nazev by udelal nevalidni dokument, ktery protistrana odmitne + * az na svem parseru - tedy pozde. Radsi se prejmenuje a rekne se to nahlas. + */ +function elementName(key, renamed) { + let name = String(key) + .normalize('NFKD') + .replace(/[̀-ͯ]/g, '') + .replace(/[^A-Za-z0-9._-]/g, '-'); + + /* + * Nazev nesmi zacinat cislici. Odriznout ji nejde: `2024` i `2025` by pak + * byly totez a v dokumentu by se srazily. Podtrzitko je platny prvni znak, + * takze se jen predsadi a klic zustane rozpoznatelny. + */ + if (/^[^A-Za-z_]/.test(name)) name = `_${name}`; + if (name === '' || name === '_') name = 'pole'; + if (name !== String(key)) renamed.set(String(key), name); + return name; +} + +export async function run(inputs, ctx) { + const renamed = new Map(); + let elements = 0; + + const nl = inputs.indent ? '\n' : ''; + const pad = (depth) => (inputs.indent ? ' '.repeat(depth) : ''); + + /** Je hodnota prazdna? `0` a `false` prazdne nejsou, ty nesou informaci. */ + const empty = (value) => + value === null || value === undefined || (typeof value === 'string' && value.trim() === ''); + + function render(key, value, depth) { + if (inputs.skipEmpty && empty(value)) return ''; + + // Seznam: kazda polozka dostane svuj prvek, obalene do prvku podle klice. + if (Array.isArray(value)) { + const items = value + .map((item) => render(inputs.itemElement, item, depth + 1)) + .filter(Boolean) + .join(''); + elements += 1; + return `${pad(depth)}<${key}>${nl}${items}${pad(depth)}${nl}`; + } + + if (value !== null && typeof value === 'object') { + const inner = Object.entries(value) + .map(([childKey, childValue]) => + render(elementName(childKey, renamed), childValue, depth + 1), + ) + .filter(Boolean) + .join(''); + elements += 1; + // Prazdny objekt se zapise jako samouzaviraci prvek, ne jako dvojice. + if (inner === '') return `${pad(depth)}<${key} />${nl}`; + return `${pad(depth)}<${key}>${nl}${inner}${pad(depth)}${nl}`; + } + + elements += 1; + if (value === null || value === undefined) return `${pad(depth)}<${key} />${nl}`; + return `${pad(depth)}<${key}>${escapeText(value)}${nl}`; + } + + const root = elementName(inputs.root, renamed); + const body = render(root, inputs.source, 0); + + const header = inputs.declaration ? `${nl}` : ''; + const xml = (header + body).trimEnd(); + + if (renamed.size > 0) { + const list = [...renamed.entries()].map(([from, to]) => `${from} -> ${to}`); + ctx.log(`Přejmenováno kvůli pravidlům XML: ${list.join(', ')}`); + } + + return { + xml, + elements, + renamed: renamed.size > 0 ? [...renamed.keys()].join(', ') : null, + }; +} diff --git a/src/data/automationStore.ts b/src/data/automationStore.ts index 2461b0b..614cd02 100644 --- a/src/data/automationStore.ts +++ b/src/data/automationStore.ts @@ -574,7 +574,7 @@ function seedDemoAutomations(): void { steps: [ { id: 'st_11', kind: 'action', serviceId: 'voicebot', operationId: 'play-scenario' }, { id: 'st_12', kind: 'action', serviceId: 'transcription', operationId: 'transcribe' }, - { id: 'st_13', kind: 'action', serviceId: 'ai-text', operationId: 'classify' }, + { id: 'st_13', kind: 'action', serviceId: 'openai', operationId: 'chat' }, { id: 'st_14', kind: 'condition', @@ -624,7 +624,7 @@ function seedDemoAutomations(): void { { id: 'st_31', kind: 'action', serviceId: 'ga4', operationId: 'run-report' }, { id: 'st_32', kind: 'action', serviceId: 'google-ads', operationId: 'campaign-report' }, { id: 'st_33', kind: 'action', serviceId: 'sklik', operationId: 'campaign-report' }, - { id: 'st_34', kind: 'action', serviceId: 'ai-text', operationId: 'generate' }, + { id: 'st_34', kind: 'action', serviceId: 'openai', operationId: 'chat' }, { id: 'st_35', kind: 'action', serviceId: 'email', operationId: 'send' }, ], }, diff --git a/src/data/services.ts b/src/data/services.ts index b6ada50..cf300ad 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -29,7 +29,12 @@ export type ServiceCategory = | 'logistika' | 'komunikace' | 'analytika' - | 'ai'; + | 'ai' + /** + * Prevody dat mezi kroky. Vlastni kategorie, ne polozka v Obecnem: + * je jich hodne a clovek je hleda jako skupinu, ne mezi webhookem a pauzou. + */ + | 'transformace'; /** * available = sluzbu umime, planned = je na roadmape. @@ -278,6 +283,7 @@ export const serviceCategories: Array<{ id: ServiceCategory; label: string }> = { id: 'komunikace', label: 'Komunikace' }, { id: 'analytika', label: 'Analytika' }, { id: 'ai', label: 'AI a hlas' }, + { id: 'transformace', label: 'Transformace dat' }, ]; export const services: Service[] = [ @@ -2306,90 +2312,6 @@ export const services: Service[] = [ }, ], }, - { - id: 'ai-text', - name: 'AI zpracování textu', - category: 'ai', - description: 'Klasifikace, extrakce dat a generování textu jazykovým modelem.', - icon: 'Sparkles', - status: 'available', - general: false, - /* - * Nebezi u nas, stejne jako OpenAI. Rozdil proti sluzbe OpenAI je v tom, - * na co se pta: tam se posila volny dotaz, tady jsou hotove ulohy - * (zaradit, vytahnout, napsat) s pevnym vystupem, na ktery se da ve strome - * navazat podminkou. - */ - appId: null, - baseUrl: 'https://api.openai.com/v1', - visibility: { mode: 'everyone', tenantIds: [], userIds: [] }, - credentials: [ - { - id: 'apiKey', - label: 'API klíč', - target: 'header', - name: 'Authorization', - prefix: 'Bearer ', - required: true, - secret: true, - hint: 'platform.openai.com, sekce API keys. Vložte jen klíč, slovo Bearer doplní portál.', - }, - { - id: 'model', - label: 'Model', - target: 'config', - name: 'model', - required: false, - secret: false, - hint: 'Bez vyplnění gpt-4o-mini. Platí pro všechny operace této služby.', - }, - { - id: 'organization', - label: 'ID organizace', - target: 'header', - name: 'OpenAI-Organization', - required: false, - secret: false, - hint: 'Jen když má účet víc organizací a útrata se má počítat konkrétní z nich.', - }, - ], - // Seznam modelu: nejlevnejsi cteci volani, ktere vyzaduje platny klic. - verifyPath: '/models', - triggers: [], - actions: [ - { - id: 'classify', - name: 'Zařadit do kategorie', - description: 'Rozhodne, do které kategorie text patří (např. téma ticketu).', - inputs: [ - { - id: 'categories', - label: 'Seznam kategorií', - kind: 'text', - required: true, - hint: 'Oddělte čárkou, například: porucha, dotaz, reklamace', - }, - { id: 'text', label: 'Text', kind: 'longtext', required: true }, - ], - outputFields: [ - { id: 'ai.category', name: 'category', type: 'string', required: true }, - { id: 'ai.confidence', name: 'confidence', type: 'number', required: true }, - ], - }, - { - id: 'extract', - name: 'Vytáhnout údaje', - description: 'Z volného textu získá strukturovaná data podle schématu.', - fields: ['Schéma polí', 'Text'], - }, - { - id: 'generate', - name: 'Vygenerovat text', - description: 'Napíše odpověď nebo shrnutí podle instrukce.', - fields: ['Instrukce', 'Vstupní data'], - }, - ], - }, /** * OpenAI je jina nez zbytek katalogu: **nebezi u nas**. Nema tedy `appId`, @@ -2552,7 +2474,7 @@ export const services: Service[] = [ { id: 'transform', name: 'Transformace dat', - category: 'obecne', + category: 'transformace', description: 'Přemapování polí, formátování a čištění dat mezi kroky.', icon: 'Shuffle', status: 'available', diff --git a/src/data/ticketStore.ts b/src/data/ticketStore.ts index 9f58b9d..e93e1ef 100644 --- a/src/data/ticketStore.ts +++ b/src/data/ticketStore.ts @@ -499,8 +499,8 @@ function seedDemoTickets(): void { }, { kind: 'action', - serviceId: 'ai-text', - operationId: 'classify', + serviceId: 'openai', + operationId: 'chat', label: 'Zařazení do kategorie', status: 'ok', response: '{ "category": "porucha", "priority": "high", "confidence": 0.88 }', @@ -602,8 +602,8 @@ function seedDemoTickets(): void { }, { kind: 'action', - serviceId: 'ai-text', - operationId: 'extract', + serviceId: 'openai', + operationId: 'chat', label: 'Vytažení údajů z textu', status: 'ok', response: '{ "type": "pozadavek na zmenu", "system": "CRM + iDoklad", "urgent": false }', @@ -684,8 +684,8 @@ function seedDemoTickets(): void { }, { kind: 'action', - serviceId: 'ai-text', - operationId: 'classify', + serviceId: 'openai', + operationId: 'chat', label: 'Zařazení do kategorie', status: 'ok', response: '{ "category": "vypadek reportu", "priority": "normal", "confidence": 0.79 }',