Files
csbot-prototype/scripts/openai.ask-about-file.js
T
JiriUhlirandClaude Opus 5 a771834e57 Realne sluzby, OpenAI, odesilani e-mailu a helpdesk
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.

OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).

Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.

Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.

Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci

Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:40:16 +02:00

148 lines
4.3 KiB
JavaScript

/**
* OpenAI: otazka nad nahranym souborem.
*
* Sluzba: https://api.openai.com/v1
* Endpoint: POST /responses
*
* Proc jiny endpoint nez `openai.chat`: soubor jako vstup umi az Responses API.
* Chat Completions by prijalo jen text, takze by se obsah musel do dotazu
* vlepit rucne - a u PDF nebo tabulky to nejde.
*
* Soubor se nejdriv nahraje krokem `openai.upload-file`, ktery vrati `fileId`.
* Rozdeleni na dva kroky je zamer: jednoho souboru se casto pta vic dotazu
* a nahravat ho pokazde znovu by stalo cas i penize.
*/
export const manifest = {
id: 'openai.ask-about-file',
name: 'Zeptat se na soubor',
description:
'Položí otázku nad souborem nahraným do OpenAI. Zvládne dokument i obrázek, ' +
'takže se hodí na vytěžení faktury, smlouvy nebo fotky.',
inputs: [
{
id: 'fileId',
label: 'ID souboru',
type: 'string',
required: true,
hint: 'Vrací ho krok Nahrát soubor jako fileId.',
},
{
id: 'kind',
label: 'Druh souboru',
type: 'string',
required: false,
default: 'document',
options: [
{ value: 'document', label: 'Dokument (PDF, text, tabulka)' },
{ value: 'image', label: 'Obrázek' },
],
hint:
'Obrázek se modelu předává jinak než dokument. Musí být nahraný ' +
's účelem Obrázek k analýze.',
},
{
id: 'prompt',
label: 'Otázka',
type: 'string',
required: true,
multiline: true,
hint: 'Například: Vypiš číslo faktury, datum splatnosti a celkovou částku.',
},
{
id: 'model',
label: 'Model',
type: 'string',
required: false,
default: 'gpt-4o-mini',
hint: 'Musí to být model, který umí číst soubory a obrázky.',
},
{
id: 'instructions',
label: 'Instrukce pro model',
type: 'string',
required: false,
multiline: true,
},
{
id: 'maxTokens',
label: 'Strop na délku odpovědi',
type: 'number',
required: false,
hint: 'V tokenech. Bez vyplnění rozhoduje model.',
},
],
outputs: [
{ id: 'text', label: 'Odpověď', type: 'string', required: true },
{ id: 'model', label: 'Použitý model', type: 'string', required: true },
{ id: 'inputTokens', label: 'Tokeny na vstupu', type: 'number', required: false },
{ id: 'outputTokens', label: 'Tokeny na výstupu', type: 'number', required: false },
{ id: 'totalTokens', label: 'Tokeny celkem', type: 'number', required: false },
],
// Cteni dokumentu trva dele nez bezny dotaz.
timeoutMs: 90000,
};
/**
* Odpoved Responses API je strom, ne jedno pole.
*
* Prochazi se cely: model muze pred odpovedi vratit i jine polozky (napriklad
* zaznam o uvaze) a brat naslepo prvni prvek by u nich vratilo prazdno.
*/
function answerText(output) {
if (!Array.isArray(output)) return null;
const parts = [];
for (const item of output) {
const content = item && item.content;
if (!Array.isArray(content)) continue;
for (const part of content) {
if (part && part.type === 'output_text' && typeof part.text === 'string') {
parts.push(part.text);
}
}
}
return parts.length > 0 ? parts.join('\n') : null;
}
export async function run(inputs, ctx) {
const { pick, text, num, need } = ctx.util;
const attachment =
inputs.kind === 'image'
? { type: 'input_image', file_id: inputs.fileId }
: { type: 'input_file', file_id: inputs.fileId };
const body = {
model: inputs.model,
input: [
{
role: 'user',
content: [attachment, { type: 'input_text', text: inputs.prompt }],
},
],
};
if (inputs.instructions) body.instructions = inputs.instructions;
if (inputs.maxTokens !== null) body.max_output_tokens = inputs.maxTokens;
const { body: answer } = await ctx.http.post('/responses', body);
if (text(pick(answer, 'status')) === 'incomplete') {
ctx.log('Odpověď je neúplná, model narazil na strop délky.');
}
const usage = pick(answer, 'usage') ?? {};
return {
text: need(answerText(pick(answer, 'output')), 'odpověď modelu'),
model: need(text(pick(answer, 'model')), 'název modelu'),
inputTokens: num(pick(usage, 'input_tokens')),
outputTokens: num(pick(usage, 'output_tokens')),
totalTokens: num(pick(usage, 'total_tokens')),
};
}