Ga naar inhoud

Functies via de API

Het concept achter tools staat in Tools.

Een functie is eigen code, gecompileerd naar WebAssembly, die het model als één tool aanroept, function__<naam>. Core draait hem binnen Core zelf, in milliseconden, met de argumenten van het model als invoer, en geeft de JSON terug die hij schrijft. Een functie roept nooit iets buiten zichzelf aan: geen klok, geen toeval, geen netwerk, geen bestanden. Hij leest zijn argumenten en, als zijn versie ze declareert, records uit je datastores die Core eerst voor hem leest; hij verandert records alleen door de schrijfacties terug te geven die hij wil, en die voert Core voor hem uit. Dezelfde invoer geeft dus altijd hetzelfde antwoord.

Gebruik een functie voor wat een model nooit mag beslissen: de prijs van een orderregel in zijn staffel, een minimumbestelling, hoe de btw wordt verdeeld. Het model mag een prijs uitleggen; het getal zelf komt uit jouw code. Gebruik een script als je code bestanden, pakketten of seconden werk nodig heeft.

Functies schrijf je alleen via de API; de console heeft er nog geen scherm voor.

Wat je sleutel nodig heeft

De capability tooling: een functie heeft geen computer nodig. Een functie die datastores leest of schrijft heeft daarnaast datastores nodig op de sleutel die hem aanroept; een sleutel zonder die capability krijgt de tool niet aangeboden, en hem noemen geeft 422, zoals bij elke tool die een sleutel niet mag hebben. Voeg files toe als je de module als bestand uploadt in plaats van in de body van het verzoek.

Een module bouwen

Een module leest één JSON-document op standaardinvoer en schrijft één JSON-document op standaarduitvoer. De invoer is {"arguments": {...}}, de argumenten die het model stuurde, met "reads" ernaast als de versie leesacties declareert. De uitvoer moet een JSON-object zijn; een versie die datastores leest of schrijft schrijft in plaats daarvan {"result": {...}, "operations": [...]} (Datastores lezen en schrijven). Sluit af met code 0 als de aanroep lukte; elke andere code is een fout.

In Rust bouw je voor wasm32-wasip1:

use std::io::{self, Read, Write};

fn main() {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).unwrap();
    let document: serde_json::Value = serde_json::from_str(&input).unwrap();
    let quantity = document["arguments"]["quantity"].as_i64().unwrap_or(0);
    let unit_cents = if quantity >= 10 { 900 } else { 1000 };
    let answer = serde_json::json!({ "total_cents": quantity * unit_cents });
    io::stdout().write_all(answer.to_string().as_bytes()).unwrap();
}
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1

Een module mag standaardinvoer, standaarduitvoer (en standaardfout, die Core weggooit), afsluiten en het lezen van zijn argumenten en omgeving gebruiken, en die laatste twee zijn altijd leeg. Hij mag ook de klok en willekeurige getallen koppelen, zoals de standaardbibliotheek van Rust doet, maar een aanroep die echt om de tijd of om willekeurige bytes vraagt stopt met trapped en zegt waarom hij vroeg, elke keer op dezelfde manier. Geef de huidige tijd of een seed mee als argument als je die nodig hebt. In Rust vraagt een HashMap met zijn standaardhasher om willekeurige bytes zodra hij wordt gemaakt; geef hem een vaste hasher, of gebruik een BTreeMap. Een module die bestanden of sockets importeert weigert Core bij het uploaden, en de fout noemt de import.

Een functie registreren

POST /functions maakt de functie en zijn versie 1. Geef de module op een van twee manieren:

  • module: het .wasm-bestand als base64-tekst, voor een module onder ongeveer 700 kB;
  • module_file_id: de id van een bestand dat je uploadde met POST /files, voor een grotere module tot 4 MiB. Je sleutel heeft hiervoor files nodig.
curl -s -X POST "$CORE_URL/functions" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d @staffelprijs.json
{
  "name": "staffelprijs",
  "description": "De prijs van één orderregel in zijn staffel. Gebruik hem voor elke prijs die je noemt.",
  "parameters": {
    "type": "object",
    "properties": { "quantity": { "type": "integer", "minimum": 1 } },
    "required": ["quantity"]
  },
  "output_schema": {
    "type": "object",
    "properties": { "total_cents": { "type": "integer" } },
    "required": ["total_cents"]
  },
  "module": "AGFzbQEAAAAB..."
}
  • parameters is het JSON Schema waaraan de argumenten van het model moeten voldoen. Argumenten die er niet aan voldoen bereiken je module nooit.
  • output_schema is het JSON Schema waaraan het antwoord van je module moet voldoen; bij een versie met reads of writes de result daarin. Een antwoord dat er niet aan voldoet is een fout, zodat het model nooit een misvormde prijs leest.
  • fuel_limit en memory_limit_bytes zijn optioneel; zie Grenzen.
  • reads en writes zijn optioneel; zie Datastores lezen en schrijven.

Het antwoord is 201 met de functie, inclusief module_sha256, de vingerafdruk van de module die je uploadde. Een module die geen WebAssembly is, iets importeert dat Core niet aanbiedt of geen _start heeft is 422 op module, met wat er mis is.

POST /functions/{id}/versions schrijft de volgende versie met dezelfde body, zonder name. Een body die gelijk is aan de laatste versie schrijft niets en antwoordt 200. GET /functions, GET /functions/{id}, GET /functions/{id}/versions en DELETE /functions/{id} werken als bij scripts. Een functie verwijderen die een datastore als schrijffunctie noemt, geeft 409 met de naam van die datastore, totdat de write_function van die datastore is gewist of gewijzigd.

Datastores lezen en schrijven

Een functie die met opgeslagen gegevens werkt, zoals een checkout, declareert wat hij leest en in welke datastores hij mag schrijven. Je module vraagt nooit zelf om een record: Core leest de gedeclareerde records voordat de module draait, als de aanroeper, en voert de schrijfacties uit die de module teruggeeft.

Lezen. reads is een lijst van hoogstens acht leesacties, in volgorde gelezen:

  • {"kind": "record", "as": "cart", "datastore": "carts", "key": "arguments.cart"} leest één record; je module vindt het onder reads.cart als {"key", "data", "etag"}, of null als er geen is.
  • {"kind": "records", "as": "stock", "datastore": "stock", "keys": "reads.cart.data.lines[*].sku"} leest een lijst sleutels, hoogstens 100; reads.stock bevat één item per sleutel, in volgorde, null voor een sleutel zonder record.

key en keys zijn JMESPath-expressies over {"arguments", "reads"}: de argumenten van de aanroep en de leesacties die vóór deze zijn gedeclareerd. Een expressie die niet compileert geeft 422 op haar veld als je de versie schrijft. Een expressie die null geeft leest niets; een die iets anders dan een sleutel geeft, of meer dan 100 sleutels, laat de aanroep mislukken met invalid_arguments voordat je module draait.

Core leest als de aanroeper. De aanroep van een bezoeker leest haar eigen winkelwagen en de gedeelde productrecords; de winkelwagen van iemand anders, en een record of datastore die de policyregels van de sleutel niet laten zien, lezen als null, precies alsof ze niet bestaan.

Schrijven. writes noemt de datastores waarin de module mag schrijven, hoogstens zestien. Elk ervan moet bestaan en bereikbaar zijn voor de sleutel die de functie aanroept. De module geeft zijn schrijfacties terug als operations in de vorm van een batch, met elke datastore bij naam; operations mag wegblijven als er niets te schrijven is. Een put vervangt het hele record, net als een losse schrijfactie. Core voert ze allemaal uit in één transactie, of geen ervan.

Een operatie op een sleutel die de aanroep las, is gebonden aan wat er gelezen is, zodat een schrijfactie nooit terechtkomt op een record dat je module niet zag. Een operatie die zelf if_match of if_absent zet, houdt die in plaats van de eerste twee rijen hieronder; de laatste rij weigert de aanroep ook dan:

De aanroep las de sleutel als Een put Een delete
een record alleen als het onveranderd is (if_match zijn etag) alleen als het onveranderd is
null alleen als er nog steeds geen is (if_absent) overgeslagen: beantwoord met deleted: false
een gedeeld record, in een aanroep voor een persoon weigert de aanroep weigert de aanroep

Een aanroep voor een persoon schrijft haar eigen records en mag nooit een gedeeld record veranderen: zo'n aanroep mislukt met denied_by_policy en er wordt niets geschreven. Een operatie op een sleutel die de aanroep niet las, voert Core uit zoals je module hem schreef.

Veranderde een record waaraan de schrijfacties gebonden zijn tussen het lezen en het schrijven, dan wordt er niets geschreven, en leest Core opnieuw en draait je module opnieuw, tot drie keer extra. Een voorwaarde die je module zelf zette en die niet meer klopt, draait op dezelfde manier opnieuw. Klopt hij na de vierde keer nog steeds niet, dan mislukt de aanroep met conflict en is er niets geschreven. Elke andere weigering, zoals gegevens die niet in het schema van de datastore passen, laat de aanroep mislukken met executor_failed, met de operatie erbij; er is niets geschreven.

Een checkout

Een functie checkout leest de winkelwagen en de voorraad van elke regel, nummert de bestelling, schrijft haar weg, verlaagt de voorraad en leegt de winkelwagen. De datastores carts, stock en orders moeten bestaan, en de sleutel die hem aanroept moet alle drie bereiken.

{
  "name": "checkout",
  "description": "Plaatst de bestelling voor een winkelwagen. Gebruik hem als de klant bevestigt.",
  "parameters": {
    "type": "object",
    "properties": { "cart": { "type": "string" } },
    "required": ["cart"]
  },
  "reads": [
    { "kind": "record", "as": "cart", "datastore": "carts", "key": "arguments.cart" },
    { "kind": "records", "as": "stock", "datastore": "stock", "keys": "reads.cart.data.lines[*].sku" }
  ],
  "writes": ["orders", "stock", "carts"],
  "module": "AGFzbQEAAAAB..."
}

Aangeroepen met {"cart": "cart-9"} leest de module dit op standaardinvoer:

{
  "arguments": { "cart": "cart-9" },
  "reads": {
    "cart": {
      "key": "cart-9",
      "data": { "lines": [{ "sku": "TSHIRT-RED-M", "quantity": 2, "price_cents": 1900 }] },
      "etag": "\"dsr_01J….3\""
    },
    "stock": [{ "key": "TSHIRT-RED-M", "data": { "left": 4 }, "etag": "\"dsr_01J….4\"" }]
  }
}

en schrijft dit op standaarduitvoer:

{
  "result": { "placed": true, "total_cents": 3800 },
  "operations": [
    { "op": "counter", "datastore": "orders", "counter": "orders" },
    { "op": "put", "datastore": "orders", "key_from": { "operation": 0, "prefix": "O-" },
      "data": { "lines": [{ "sku": "TSHIRT-RED-M", "quantity": 2 }], "total_cents": 3800 },
      "if_absent": true },
    { "op": "put", "datastore": "stock", "key": "TSHIRT-RED-M", "data": { "left": 2 } },
    { "op": "delete", "datastore": "carts", "key": "cart-9" }
  ]
}

De teller geeft het volgende bestelnummer en de bestelling krijgt de sleutel O-<nummer>. De operaties op voorraad en winkelwagen noemen sleutels die de aanroep las, dus bindt Core ze aan de gelezen etags: twee klanten die tegelijk afrekenen nemen nooit allebei dezelfde voorraad. De tweede aanroep leest opnieuw, rekent met de voorraad die de eerste overliet, en kan antwoorden dat een artikel op is als er niets meer over is.

Draai een checkout met de sleutel van je backend. De voorraad is gedeeld: elke bezoeker leest dezelfde records. Een aanroep voor een persoon, vanuit een gesprek met een bezoeker of met een subject token, mag geen gedeeld record veranderen, dus laat zijn schrijfactie op de voorraad de aanroep mislukken met denied_by_policy. Roep de checkout aan met de eigen sleutel van je backend, die bij geen persoon hoort, en bewaar de winkelwagens als gedeelde records met een sleutel per bezoeker, zoals hier.

Bij een versie met reads of writes bevat de text van het antwoord Core's {"result", "operations"}: je result, en wat elke operatie deed, zodat je het bestelnummer kent:

{
  "result": { "placed": true, "total_cents": 3800 },
  "operations": [
    { "op": "counter", "datastore": "orders", "key": null, "etag": null, "created": null, "value": 7, "deleted": null },
    { "op": "put", "datastore": "orders", "key": "O-7", "etag": "\"dsr_01J….1\"", "created": true, "value": null, "deleted": null },
    { "op": "put", "datastore": "stock", "key": "TSHIRT-RED-M", "etag": "\"dsr_01J….5\"", "created": false, "value": null, "deleted": null },
    { "op": "delete", "datastore": "carts", "key": "cart-9", "etag": null, "created": null, "value": null, "deleted": true }
  ]
}

In een gesprek gebeuren de schrijfacties op het moment dat het model de functie aanroept, niet aan het eind van de beurt.

Grenzen

Grens Standaard Wat een aanroep die hem bereikt antwoordt
Brandstof, ongeveer één eenheid per instructie 100.000.000 per aanroep; een versie mag tot 1.000.000.000 vragen fuel_exhausted
Geheugen 16 MiB per aanroep; een versie mag tot 64 MiB vragen memory_exceeded
Tijd 2 seconden per aanroep, hoeveel brandstof er ook over is timeout
Grootte van het antwoord 1 MiB invalid_response
Grootte van de module 4 MiB 422 bij het uploaden
Leesacties per versie 8 422 bij het schrijven
Sleutels per leesactie records 100 invalid_arguments
Records die de leesacties aan je module geven samen 1 MiB invalid_arguments
Datastores waarin een versie schrijft 16 422 bij het schrijven
Operaties die één aanroep teruggeeft 100 invalid_response

Twee leesacties met dezelfde as, of een datastore die twee keer in writes staat, geeft 422 als je de versie schrijft.

Brandstof is waar je tegen schrijft: die telt op elke machine hetzelfde, dus een aanroep die op je laptop past, past ook in productie. De beheerder van je omgeving kan andere waarden instellen.

Uitproberen

Roep de functie rechtstreeks aan, met de argumenten die een model zou sturen:

curl -s -X POST "$CORE_URL/tools/function__staffelprijs/call" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"arguments": {"quantity": 12}}'

Het antwoord is het object tool_call van Een tool rechtstreeks aanroepen: text bevat de JSON die je module schreef (bij een versie met reads of writes Core's {"result", "operations"}), en error_code zegt waarom een aanroep mislukte:

error_code Betekent
invalid_arguments de argumenten voldeden niet aan parameters, de expressie van een leesactie gaf geen bruikbare sleutel of te veel, of de gelezen records waren samen meer dan 1 MiB; je module draaide niet
trapped je module stopte op een fout, zoals een panic of een deling door nul, of vroeg om de tijd of om willekeurige bytes
fuel_exhausted je module had geen brandstof meer
memory_exceeded je module had geen geheugen meer
timeout je module had geen tijd meer
invalid_response het antwoord was te groot, geen JSON-object, of voldeed niet aan output_schema; of, bij een versie met reads of writes, het was geen {"result", "operations"} of er stond een ander veld naast, een operatie noemde een datastore die niet in writes staat of die de sleutel niet bereikt, of een operatie is er geen die een batch aanneemt; er is niets geschreven
executor_failed je module sloot af met een andere code dan 0, er liepen te veel aanroepen tegelijk, of een operatie werd geweigerd; er is niets geschreven
conflict records die de aanroep las bleven veranderen voordat zijn schrijfacties werden uitgevoerd, vier keer op rij; er is niets geschreven
denied_by_policy een aanroep voor een persoon probeerde een gedeeld record te veranderen dat hij las, of een oudere versie die je vastpinde leest of schrijft datastores en de sleutel heeft geen datastores; er is niets geschreven

Voeg "version": 1 toe om een oudere versie te draaien.

Aanbieden aan het model

Noem hem in de tools van een verzoek als {"type": "tool", "name": "function__staffelprijs"}, in een agent, in een toolset (function__* neemt elke functie) of onder een alias, zoals elke andere tool. Policyregels over tool beslissen op zijn naam.

Verbruik en audit

  • Elke aanroep schrijft één verbruiksrecord tool_invocation met de naam function__<naam>.
  • Dezelfde argumenten aan dezelfde versie geven hetzelfde antwoord, dus Core mag een herhaling uit het geheugen beantwoorden zonder je module opnieuw te draaien. Een versie met reads of writes draait altijd.
  • Elke schrijfactie laat de auditregels achter die een schrijfactie in een datastore achterlaat.
  • Het auditspoor legt function.created, function.version_created, function.updated, function.deleted vast, en per aanroep function.called met de versie, vingerafdrukken van de invoer en het antwoord, hoe de aanroep eindigde, de brandstof die hij gebruikte, hoe vaak hij draaide en hoeveel operaties hij uitvoerde. Argumenten, gelezen records en antwoorden komen nooit in het auditspoor.