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 metPOST /files, voor een grotere module tot 4 MiB. Je sleutel heeft hiervoorfilesnodig.
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..."
}
parametersis het JSON Schema waaraan de argumenten van het model moeten voldoen. Argumenten die er niet aan voldoen bereiken je module nooit.output_schemais het JSON Schema waaraan het antwoord van je module moet voldoen; bij een versie metreadsofwritesderesultdaarin. Een antwoord dat er niet aan voldoet is een fout, zodat het model nooit een misvormde prijs leest.fuel_limitenmemory_limit_byteszijn optioneel; zie Grenzen.readsenwriteszijn 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 onderreads.cartals{"key", "data", "etag"}, ofnullals er geen is.{"kind": "records", "as": "stock", "datastore": "stock", "keys": "reads.cart.data.lines[*].sku"}leest een lijst sleutels, hoogstens 100;reads.stockbevat één item per sleutel, in volgorde,nullvoor 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_invocationmet de naamfunction__<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
readsofwritesdraait 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.deletedvast, en per aanroepfunction.calledmet 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.