Transformace maji svou kategorii, pribylo XML

Odstranena sluzba AI zpracovani textu. Delala totez co OpenAI, jen jinymi
slovy, a dva zpusoby jak udelat jednu vec znamenaji jen otazku, ktery pouzit.
Ukazkove stromy a stopy, ktere na ni odkazovaly, miri na openai.chat.

- nova kategorie Transformace dat. Driv byly mezi obecnymi sluzbami vedle
  webhooku a pauzy; ve chvili kdy jich je vic nez tri, hleda je clovek 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 toho 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, jinak by 2024 a 2025 byly totez.
- navrh cele skupiny transformaci v 13-transformace-dat.md: formaty, pole
  a hodnoty, seznamy, text a kontrola, vcetne toho co v seznamu zamerne neni.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-28 09:05:27 +02:00
co-authored by Claude Opus 5
parent aa0a783095
commit e1c52e3da3
9 changed files with 287 additions and 428 deletions
+80
View File
@@ -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
```
+25
View File
@@ -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
-106
View File
@@ -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,
};
}
-114
View File
@@ -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,
};
}
-114
View File
@@ -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,
};
}
+166
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/**
* 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)}</${key}>${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)}</${key}>${nl}`;
}
elements += 1;
if (value === null || value === undefined) return `${pad(depth)}<${key} />${nl}`;
return `${pad(depth)}<${key}>${escapeText(value)}</${key}>${nl}`;
}
const root = elementName(inputs.root, renamed);
const body = render(root, inputs.source, 0);
const header = inputs.declaration ? `<?xml version="1.0" encoding="UTF-8"?>${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,
};
}
+2 -2
View File
@@ -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' },
],
},
+8 -86
View File
@@ -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',
+6 -6
View File
@@ -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 }',