ZAIA by ZCLOUD

Implementační návod

Připojení systému (městský portál, e-shop, helpdesk) je adaptér ve vašem backendu. Nikdy nevyžaduje změnu v ZAIA. Příklady používají doménu produktů.

Obsah

Kdo za co ručí

ZAIA nikdy nerozhoduje, kdo smí záznam vidět, a nikdy nestahuje URL zdrojů. Připojení nového systému je adaptér ve vašem backendu, ne změna v API.

Váš backendZAIA
Definice polí, kontext, historie konverzaceAPI klíče a tenanti
Validace parametrů a obchodní pravidlaVolání modelu a kontrola jeho výstupu
Vyhledávání ve vlastních datech a autorizace uživateleMěsíční kvóty a limit souběhu
Výběr záznamů, které se stanou zdrojiKontrola, že citované zdroje byly na vstupu
Zobrazení odpovědi a karet zdrojůNic neukládá: žádné dotazy, podklady ani odpovědi

Krok 0: poskytnutí přístupu

Přístup zřizuje provozovatel, ne integrovaná aplikace, pomocí admin klíče (hlavička X-Admin-Key, nastavený v konfiguraci ZAIA, alespoň 32 znaků).

POST /admin/tenants
{"name":"Acme Shop","monthlyRequestLimit":20000}

POST /admin/tenants/{id}/keys
{"name":"backend production"}

Limit je 1 až 1 000 000 požadavků měsíčně. Odpověď na vytvoření klíče obsahuje surový key jen jednou, ukládá se pouze jeho SHA-256 hash.

Klíč uložte do správce tajemství vašeho backendu a používejte ho jen na serveru jako Authorization: Bearer <key>. Nikdy ho nedávejte do prohlížeče ani mobilní aplikace. Admin klíč z aplikace nepoužívejte vůbec.

Testovací klíče

Pro vývoj a CI použijte testovací klíč. Operátor ho vytvoří stejným endpointem s příznakem test:

POST /admin/tenants/{id}/keys
{"name":"integration test","test":true}

Odpověď (201):

{"id":"<guid>","key":"aia_test_<43 znaků>","prefix":"aia_test_Ab3","name":"integration test","test":true}

Živé klíče mají tvar tvm_... a "test":false. Režim určuje uložený klíč, ne jeho prefix. GET /admin/tenants/{id}/keys vrací u každého klíče id, name, prefix, test a revokedAt.

S testovacím klíčem /v1/interpretations a /v1/answers provedou plnou validaci vstupu (stejné 400 s detail a field) a vrátí deterministickou odpověď:

  • bez volání modelu, bez čerpání kvóty a bez záznamu v usage,
  • hlavička X-Test-Mode: true, bez hlaviček X-Quota-*, X-Request-Id zůstává,
  • usage: {"inputTokens":0,"outputTokens":0,"known":true}.

Interpretace: status ready, parameters obsahují každé deklarované pole se vzorovou hodnotou. string: první allowedValue, jinak "test". integer: 1. boolean: true. date: dnešní UTC datum ve tvaru yyyy-MM-dd. stringArray: [první allowedValue], jinak ["test"]. Otázka obsahující [clarify] vrátí status needs_clarification, parameters {} a clarification "Testovací režim: upřesněte prosím dotaz." (pro jiný locale než cs-CZ anglicky "Test mode: please clarify the question.")

Odpověď: status answered, answer "Testovací odpověď z podkladu: {title prvního zdroje}." (pro jiný locale než cs-CZ anglicky "Test answer from source: {title}."), sources obsahují jen první zdroj. Otázka obsahující [insufficient] nebo prázdné sources vrátí insufficient_sources s běžným lokalizovaným textem a sources [].

GET /v1/usage s testovacím klíčem vrátí nulovou spotřebu (jen monthlyRequestLimit zůstává), X-Test-Mode: true a žádné X-Quota-* hlavičky. Testovací klíč funguje i při vyčerpané měsíční kvótě. Zrušený testovací klíč nebo vypnutý tenant vrátí 401.

Příklad odpovědi interpretace v testovacím režimu pro pole city (string, allowedValues ["Praha","Brno"]), limit (integer), open (boolean), from (date) a tags (stringArray):

{"requestId":"...","status":"ready","parameters":{"city":"Praha","limit":1,"open":true,"from":"2026-10-29","tags":["test"]},"clarification":null,"usage":{"inputTokens":0,"outputTokens":0,"known":true}}

Doporučený postup: vývoj a CI běží proti testovacímu klíči. V produkci přepněte jen tajemství na živý klíč, bez změny kódu.

Klientské knihovny

Klientská knihovna pro PHP a C# je jeden soubor bez závislostí. Zkopírujte ho do projektu, nic dalšího neinstalujete.

Klíč patří jen na server. Ve vývoji a CI použijte testovací klíč aia_test_..., v produkci přepněte jen tajemství.

PHP

require __DIR__ . '/AiAsistentClient.php';
use AiAsistent\{Client, Field};

$aia = new Client('https://ai.example.invalid', getenv('AIA_KEY'));

// 1. Turn the question into parameters.
$r = $aia->interpret($_GET['q'], [
    Field::string('category', 'Product category', true, ['outdoor', 'books']),
    Field::integer('maxPrice', 'Maximum price in CZK'),
], ['context' => ['currency' => 'CZK']]);
if ($r->needsClarification()) { exit(htmlspecialchars($r->clarification())); }

// 2. Your own search (validate $r->parameters(), apply your authorization).
$sources = my_search($r->parameters()); // [['id' => ..., 'title' => ..., 'text' => ..., 'url' => 'https://...'], ...]

// 3. Answer from those sources.
$a = $aia->answer($_GET['q'], $sources);
echo '<p>' . htmlspecialchars($a->answer()) . '</p>'; // plain text, always escape
foreach ($a->sources() as $s) {
    echo '<a href="' . htmlspecialchars($s['url']) . '">' . htmlspecialchars($s['title']) . '</a>';
}

C#

using AIAsistent.Client;

var ai = new AiAsistentClient(new Uri("https://ai.example.invalid"), apiKey);

var q = await ai.InterpretAsync(new("Tent under 3000 CZK in stock?",
[
    Field.String("category", "Product category.", true, "outdoor", "books"),
    Field.Integer("maxPrice", "Maximum price in CZK."),
]) { Context = new { currency = "CZK" } });

if (q.NeedsClarification) return Ask(q.Clarification!);
var hits = MySearch(q.Get<string>("category"), q.Get<int?>("maxPrice")); // your data, your authorization rules

var a = await ai.AnswerAsync(new(question, hits.Select(h => new Source(h.Id, h.Title, h.Text, h.Url)).ToList()));
Render(a.IsAnswered ? a.Answer : "Sorry, nothing found."); // plain text, escape it; a.Sources = cited cards

Chyby a opakování

Neúspěšná odpověď vyhodí výjimku: PHP ApiException (pole status, errorCode, detail, field, traceId, retryAfter), C# AiAsistentException (pole StatusCode, Code, Detail, Field, TraceId, RetryAfter). Síťovou chybu nebo timeout hlásí PHP jako TransportException. Klienti nedělají žádné automatické opakování. Timeout nebo výpadek u placeného volání je nejednoznačný, model mohl požadavek zpracovat a zaúčtovat, proto ho neopakujte slepě. Při 502 nebo 503 opakujte jen vědomě a s limitem. Výchozí timeout je 40 s, časový limit modelu na serveru je 30 s.

Krok 1: definice polí

Filtry vašeho vyhledávání popište jako fields (1 až 20 položek).

Návrh z ukázkového záznamu vaší databáze připraví pomocník pro definici polí (běží jen v prohlížeči). Požadavky a odpovědi s testovacím klíčem si můžete vyzkoušet v playgroundu.

VlastnostPravidlo
nameunikátní, ^[A-Za-z][A-Za-z0-9_]{0,49}$
typestring, integer, boolean, date, stringArray
descriptionpovinný, nejvýše 500 znaků
requiredboolean
allowedValuesvolitelné, 1 až 100 různých řetězců po nejvýše 100 znacích, jen pro string a stringArray

Data se vracejí ve tvaru yyyy-MM-dd. Pro uzavřené množiny hodnot vždy použijte allowedValues. Vrací se kanonická povolená hodnota; rozdíly ve velikosti písmen a české diakritice ve výstupu modelu se tolerují (Kultura se vrátí jako kultura).

Krok 2: kontext, historie, jazyk

  • question je povinná, neprázdná, nejvýše 2000 znaků.
  • context: JSON objekt, nejvýše 8000 serializovaných znaků. Pošlete fakta, která model nemůže znát (měna, časové pásmo, povolená data, město uživatele). Bere se jako data, ne jako instrukce.
  • history (volitelné): nejvýše 10 položek, role user nebo assistant, obsah do 2000 znaků.
  • locale: jeden ze 17 podporovaných, cs-CZ, sk-SK, en-US, de-DE, pl-PL, uk-UA, fr-FR, es-ES, it-IT, pt-BR, ru-RU, tr-TR, ar-SA, zh-CN, ja-JP, ko-KR, id-ID. Výchozí je cs-CZ. Regionální varianta (např. de-AT) se převede na podporovaný jazyk, jiná hodnota vrátí 400 validation_error.
  • Celé tělo požadavku je omezeno na 128 KiB.

Krok 3: vlastní hledání s autorizací

Vrácené parametry nejdřív zvalidujte (rozsahy, vlastnictví, obchodní pravidla), pak dotazujte vlastní databázi. Použijte vlastní pravidla viditelnosti (publikováno, skladem, patří tomuto uživateli). Odpověď modelu nikdy není autorizační rozhodnutí. Neodesílejte nepublikované, skryté ani zrušené záznamy.

Krok 4: záznamy jako zdroje a vykreslení

Pošlete nejvýše 20 záznamů jako sources:

VlastnostLimit
idunikátní, do 100 znaků
titledo 200 znaků
textdo 4000 znaků; součet textů všech zdrojů nejvýše 40 000 znaků
urlabsolutní HTTPS, do 2048 znaků, bez přihlašovacích údajů
updatedAtvolitelné

Pole answer vykreslete jako escapovaný prostý text a nikdy ho neberte jako HTML. Vrácené sources zobrazte jako karty s odkazy na vaše vlastní stránky. Samotná odpověď neobsahuje odkazy.

Úplné příklady (doména produktů)

POST /v1/interpretations

Požadavek:

POST /v1/interpretations
Authorization: Bearer <tenant key>
Content-Type: application/json

{
  "question": "Do you have a tent under 3000 CZK in stock?",
  "context": { "currency": "CZK" },
  "fields": [
    { "name": "category", "type": "string", "description": "Product category.", "required": true, "allowedValues": ["outdoor", "books"] },
    { "name": "maxPrice", "type": "integer", "description": "Maximum price in CZK.", "required": false },
    { "name": "inStock", "type": "boolean", "description": "Only items in stock.", "required": false }
  ]
}

Odpověď. status je ready nebo needs_clarification; usage nese počty tokenů od poskytovatele, known je false, pokud je nenahlásil.

{
  "requestId": "...",
  "status": "ready",
  "parameters": { "category": "outdoor", "maxPrice": 3000, "inStock": true },
  "clarification": null,
  "usage": { "inputTokens": 812, "outputTokens": 64, "known": true }
}

U needs_clarification obsahuje clarification otázku pro uživatele a povinné parametry mohou chybět. Pole se vrací s deklarovanými typy, backend je přesto musí znovu zvalidovat a vynutit.

POST /v1/answers

Požadavek se záznamy, které vrátilo vaše hledání pro dané parametry:

POST /v1/answers
Authorization: Bearer <tenant key>
Content-Type: application/json

{
  "question": "Do you have a tent under 3000 CZK in stock?",
  "sources": [
    { "id": "tent", "title": "Family tent", "text": "Family tent: Four-person tent for family trips. Price 2490 CZK.", "url": "https://shop.example/products/tent", "updatedAt": "2026-09-30T08:00:00+00:00" }
  ]
}

Odpověď. status je answered nebo insufficient_sources; sources jsou jen vstupní záznamy, které model citoval.

{
  "requestId": "...",
  "status": "answered",
  "answer": "Yes, the Family tent is available for 2490 CZK.",
  "sources": [
    { "id": "tent", "title": "Family tent", "url": "https://shop.example/products/tent", "updatedAt": "2026-09-30T08:00:00+00:00" }
  ],
  "usage": { "inputTokens": 640, "outputTokens": 48, "known": true }
}

Prázdné pole sources vrátí deterministickou lokalizovanou odpověď insufficient_sources, bez volání modelu a bez čerpání kvóty. Členství zdroje se ověřuje, ale to neprokazuje, že každá věta je podložena citovaným textem.

curl a C#

Příklady jsou serverové. Hotovou sadu požadavků si stáhněte jako Postman kolekci; návod existuje i jako Markdown a v llms.txt pro AI agenty. Adresu https://ai.example.invalid nahraďte adresou, kterou dostanete při zřízení přístupu.

curl -sS https://ai.example.invalid/v1/answers \
  -H "Authorization: Bearer $AIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"Do you have a tent?","sources":[]}'
using System.Net.Http.Headers;
using System.Net.Http.Json;

// Server-side only: the key comes from a secret store, never from a client app.
var http = new HttpClient
{
    BaseAddress = new Uri("https://ai.example.invalid/"),
    // Must exceed the service's own 30 s model timeout.
    Timeout = TimeSpan.FromSeconds(40)
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AIA_KEY"));

var response = await http.PostAsJsonAsync("v1/answers", new
{
    question = "Do you have a tent under 3000 CZK in stock?",
    sources = new[]
    {
        new { id = "tent", title = "Family tent",
              text = "Family tent: Four-person tent. Price 2490 CZK.",
              url = "https://shop.example/products/tent" }
    }
});

if (!response.IsSuccessStatusCode)
{
    // ProblemDetails: log "code" and "traceId", never the key.
    Console.Error.WriteLine(await response.Content.ReadAsStringAsync());
    return;
}
var json = await response.Content.ReadAsStringAsync(); // render "answer" as escaped plain text

Rychlý start v PHP

Bez knihovny, jen cURL. Jednodušší je klientská knihovna. Čisté PHP 8 s rozšířením ext-curl, bez závislostí. Jen na serveru, klíč nikdy do prohlížeče. Tok: interpretace, vlastní hledání (zástupná funkce my_search), odpověď.

<?php
// Server-side only: the key comes from a secret store / environment, never from a client.
const AIA_BASE = 'https://ai.example.invalid';

function aia_post(string $path, array $body): array
{
    $headers = [];
    $ch = curl_init(AIA_BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('AIA_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        // Must exceed the service's own 30 s model timeout.
        CURLOPT_TIMEOUT => 40,
        // Collect response headers (X-Quota-Remaining, Retry-After).
        CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
            if (str_contains($line, ':')) {
                [$k, $v] = explode(':', $line, 2);
                $headers[strtolower(trim($k))] = trim($v);
            }
            return strlen($line);
        },
    ]);
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($raw === false) {
        // A timeout is ambiguous (the call may have been billed): do not retry blindly.
        throw new RuntimeException('ZAIA network error or timeout');
    }
    return [$status, json_decode($raw, true) ?? [], $headers];
}

function aia_call(string $path, array $body): array
{
    [$status, $json, $headers] = aia_post($path, $body);
    if (isset($headers['x-quota-remaining'])) {
        error_log('ZAIA quota remaining: ' . $headers['x-quota-remaining']);
    }
    if ($status === 429) {
        // concurrency_limit or quota_exceeded: wait for Retry-After, no tight loop.
        throw new RuntimeException('ZAIA 429, retry after ' . ($headers['retry-after'] ?? '?') . ' s');
    }
    if ($status === 400) {
        // validation_error: "detail" is the message, "field" the JSON path of the bad input.
        throw new InvalidArgumentException(($json['detail'] ?? 'invalid request') . ' [' . ($json['field'] ?? '?') . ']');
    }
    if ($status >= 400) {
        // Log "code" and "traceId" (also in X-Request-Id), never the key.
        throw new RuntimeException('ZAIA error ' . $status . ' ' . ($json['code'] ?? '') . ' ' . ($json['traceId'] ?? ''));
    }
    return $json;
}

$question = 'Do you have a tent under 3000 CZK in stock?';

// 1) Interpretation: question to typed parameters.
$interp = aia_call('/v1/interpretations', [
    'question' => $question,
    'context' => ['currency' => 'CZK'],
    'fields' => [
        ['name' => 'category', 'type' => 'string', 'description' => 'Product category.', 'required' => true, 'allowedValues' => ['outdoor', 'books']],
        ['name' => 'maxPrice', 'type' => 'integer', 'description' => 'Maximum price in CZK.', 'required' => false],
    ],
]);

// 2) Your own search with your own authorization (placeholder).
//    Validate $interp['parameters'] first; send only records the user may see.
$sources = my_search($interp['parameters'] ?? []); // up to 20 records {id, title, text, url}

// 3) Answer grounded in those sources.
$answer = aia_call('/v1/answers', ['question' => $question, 'sources' => $sources]);

// Render as escaped plain text, never as HTML.
echo htmlspecialchars($answer['answer'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

Kdy přeskočit interpretaci

Pokud váš systém už ví, co hledat (formulář s filtry, textové hledání v FAQ), nevolejte POST /v1/interpretations. Spusťte hledání a zavolejte rovnou POST /v1/answers. Ušetříte jednu placenou operaci na otázku. Doména faq v ukázce takto funguje.

Chyby

Chyby jsou ProblemDetails se stabilním code a traceId. Logujte traceId, nikdy klíč.

StatusVýznamCo udělat
400validation_error nebo invalid_request: neplatný vstup (limity, duplicitní názvy polí, špatná URL zdroje, null položky, vadný JSON). Obsahuje detail (anglická zpráva) a field (JSON cesta vadného vstupu, např. sources[4].url)Opravit požadavek. Neopakovat beze změny.
401unauthorized: neznámý, zrušený nebo vypnutý klíč (jedna obecná odpověď)Zkontrolovat klíč a tenanta, neopakovat ve smyčce.
413request_too_large: tělo přes 128 KiBPoslat méně nebo kratší zdroje.
429concurrency_limit nebo quota_exceededPočkat na hlavičku Retry-After (u kvóty do dalšího měsíce UTC), pak opakovat.
502Chyba poskytovatele nebo neplatný výstup modelu (provider_error, provider_invalid_response, provider_refused)Zobrazit náhradní obsah. Pokus mohl být započítán.
503provider_unavailable nebo service_unavailable (chyba databáze, i při ověření klíče)Zobrazit náhradní obsah, později zkusit znovu s odstupem.
504provider_timeoutNeopakovat naslepo.

Příklad těla chyby 400:

{ "type": "about:blank", "title": "Invalid request", "status": 400,
  "detail": "source URL must be an absolute HTTPS URL without credentials.",
  "code": "validation_error", "field": "sources[4].url", "traceId": "0HN..." }

ZAIA placené operace sám neopakuje a nenabízí idempotenci. Timeout je nejednoznačný: poskytovatel mohl požadavek zpracovat a zpoplatnit a neúspěšná volání si drží rezervaci kvóty. 502 a 504 opakujte jen záměrně, s limitem, nikdy v automatické smyčce. Váš vlastní HTTP timeout má být delší než 30sekundový timeout modelu v službě.

Hlavičky odpovědi

  • X-Request-Id: na každé odpovědi /v1, shodné s requestId/traceId v těle. Uvádějte ho při podpoře.
  • X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset: na odpovědích /v1/interpretations a /v1/answers, kde proběhla rezervace kvóty (i chyby a 429 quota_exceeded), a na GET /v1/usage (ne s testovacím klíčem). X-Quota-Reset je začátek dalšího měsíce UTC ve formátu ISO 8601 (2026-11-01T00:00:00Z). Odpovědi s prázdnými sources (bez volání modelu) quota hlavičky nenesou.

Rotace klíčů a spotřeba

  1. Vytvořte druhý klíč (POST /admin/tenants/{id}/keys).
  2. Přepněte volající backend na nový klíč.
  3. Starý klíč zrušte: DELETE /admin/tenants/{id}/keys/{keyId}. Zrušení platí okamžitě.

Tenanta lze vypnout přes PATCH /admin/tenants/{id} s {"enabled":false}. Aktuální měsíční (UTC) počítadla vrací GET /v1/usage.

Kontrolní seznam integrace

  • Tenant vytvořen, klíč vydán jednou a uložen na serveru ve správci tajemství.
  • Pole definována v rámci limitů, pro uzavřené množiny použito allowedValues.
  • Kontext nese časové pásmo, údaje závislé na jazyku a vše, co model nemůže znát.
  • Interpretace přeskočena, pokud jsou filtry už známé.
  • Interpretované parametry validuje backend a vynucuje obchodní pravidla.
  • Hledání uplatňuje autorizaci a viditelnost (nepublikované, skryté a zrušené záznamy se nikdy neposílají).
  • Nejvýše 20 zdrojů, text do 4000 znaků, absolutní HTTPS URL na vaší doméně.
  • Odpověď vykreslena jako escapovaný prostý text, karty zdrojů vytvořeny z vrácených sources.
  • Prázdný výsledek, needs_clarification a insufficient_sources mají cestu pro uživatele.
  • Tabulka chyb implementována, placené timeouty se nezkouší naslepo znovu.
  • Ruční kontrola kvality provedena nad evaluační sadou (nikdy se nespouští automaticky).

Referenční adaptéry

Ukázka samples/IntegrationDemo předvádí tři různé domény nad stejným nezměněným API: městské akce (data, časové pásmo, zrušení), produkty (výčty a dostupnost) a FAQ (volný text, bez interpretace, nepublikovaný článek se nikdy neposílá). Offline spuštění:

dotnet run --project samples/IntegrationDemo -- --offline --domain faq
dotnet run --project samples/IntegrationDemo -- --offline --domain events
dotnet run --project samples/IntegrationDemo -- --offline --domain products