Ga naar inhoud

Grafen via de API

Het concept achter kennis staat in Kennis.

Een kennisgraaf bevat de entiteiten waar je documenten over gaan, zoals bedrijven, personen, producten en contracten, en de relaties daartussen. Een schema dat je zelf schrijft bepaalt welke soorten er bestaan. Een index beantwoordt "welke passages lijken op deze vraag"; een graaf beantwoordt "wat hangt hiermee samen, en hoe".

Je sleutel heeft de capability knowledge nodig. Een graaf heeft geen eigen capability. Alleen een schema laten voorstellen vraagt daarnaast assist. Hetzelfde doe je met de muis in Grafen in de console.

Schema, feiten en bronnen

Wat het is
Schema de entiteitstypes en relatietypes die de graaf mag bevatten. Zonder schema kan er niets in de graaf worden geschreven
Entiteit één ding van een type, met een naam, aliassen en eigenschappen: het bedrijf Acme
Relatie een verband van een type tussen twee entiteiten, met eigen eigenschappen: Acme levert Widget
Feit een entiteit of een relatie. Een feit is manual (je schreef het zelf) of extracted (uit een bron gelezen)
Bron het document waaruit een uitgelezen feit komt. Elk uitgelezen feit noemt zijn bronnen

1. Een graaf maken

curl -s -X POST "$CORE_URL/knowledge/graphs" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{
      "name": "leveranciers",
      "description": "Leveranciers, hun producten en de stoffen daarin.",
      "visibility": "shared",
      "schema": {
        "entity_types": [
          {"name": "Company", "description": "Een bedrijf dat levert of afneemt.",
           "properties": {"type": "object", "properties": {"country": {"type": "string"}}}},
          {"name": "Product", "description": "Een product dat een bedrijf levert.",
           "properties": {"type": "object", "properties": {"sku": {"type": "string"}}},
           "identity_keys": ["sku"]}
        ],
        "relation_types": [
          {"name": "supplies", "description": "Het bedrijf levert het product.",
           "from": ["Company"], "to": ["Product"],
           "properties": {"type": "object", "properties": {"since": {"type": "integer"}}}}
        ]
      }
    }'

Het antwoord is 201 met de graaf. Het id begint met kg_; de voorbeelden hieronder gebruiken $GRAPH_ID daarvoor.

Veld Wat het doet
name uniek binnen de organisatie. Een regel in de policy noemt de graaf bij deze naam, dus hij wijzigt daarna niet meer
description wat de graaf bevat, hoogstens 1.024 tekens
visibility shared of per_person; ligt vast bij het aanmaken (Wie welke feiten ziet)
schema de eerste schemaversie. Laat je het weg, dan heeft de graaf geen schema en geen feiten tot je PUT .../schema stuurt
labels de eigen indeling van je organisatie, als label en waarde; regels over Kennisgrafen lezen ze
collection_ids, ingest_pipeline, embedding_model waaruit en hoe de graaf wordt gevuld (Vullen uit bronnen)

GET /knowledge/graphs geeft de grafen die je sleutel bereikt, GET /knowledge/graphs/{id} één graaf. PATCH wijzigt description, labels, collection_ids, ingest_pipeline en embedding_model; de naam en de zichtbaarheid niet. DELETE verwijdert de graaf met al zijn schemaversies en feiten. De collecties en hun bronnen blijven bestaan.

2. Het schema

Een entiteitstype heeft:

  • name: uniek binnen het schema, zoals Company. Begint met een hoofdletter, daarna letters, cijfers en _, hoogstens 64 tekens;
  • description: wat onder het type valt. Een extractiemodel leest dit om te bepalen wat erbij hoort, dus schrijf het zoals je het een collega zou uitleggen. Hoogstens 1.024 tekens;
  • properties: een JSON Schema (Draft 2020-12) van de eigenschappen. Alleen verwijzingen binnen hetzelfde document worden gevolgd;
  • identity_keys: de eigenschappen die één entiteit identificeren, hoogstens acht. Twee uitgelezen entiteiten met dezelfde waarden zijn dezelfde entiteit. Zonder sleutels beslist de genormaliseerde naam: hoofdletters, spaties en leestekens eromheen tellen niet mee.

Een relatietype heeft een name (begint met een kleine letter, daarna kleine letters, cijfers en _, hoogstens 64 tekens, zoals supplies), een description, from en to (elk een lijst entiteitstypes waartussen de relatie mag lopen) en properties.

Een schema heeft minstens één en hoogstens 50 entiteitstypes, en hoogstens 100 relatietypes.

Tussen twee entiteiten bestaat een relatie van één type hoogstens één keer. Komt een verband meerdere keren voor, zoals een contract per jaar, maak er dan een eigen entiteitstype van.

Een nieuwe versie schrijven

curl -s -X PUT "$CORE_URL/knowledge/graphs/$GRAPH_ID/schema" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d @schema.json

De body is het hele schema, in dezelfde vorm als schema hierboven. Verschilt de inhoud van de laatste versie, dan schrijft Core een nieuwe versie; is hij gelijk, dan gebeurt er niets. Versies zijn onveranderlijk en genummerd vanaf 1.

  • GET .../schema geeft de laatste versie, GET .../schema/versions alle versies en GET .../schema/versions/{version} één versie.
  • Een nieuwe versie controleert niet opnieuw wat er al in de graaf staat. Elk feit onthoudt in schema_version tegen welke versie het is gecontroleerd.
  • Schrijven en bevragen gebruiken altijd de laatste versie. Een schema dat een entiteitstype of relatietype weglaat waar de graaf nog feiten van bevat, wordt geweigerd met een 409 die elk type noemt en zegt hoeveel feiten het hebben; verwijder die feiten eerst.
  • Het antwoord op een schrijfactie die verandert wat er wordt uitgelezen, draagt reindex_required: true. Core leest dan niets vanzelf opnieuw uit; dat start je zelf (Opnieuw uitlezen).

Een schema laten voorstellen

POST /assist/graph-schema laat het hulpmodel van je organisatie een schema schrijven. Je sleutel heeft daarvoor naast knowledge de capability assist nodig; zonder assist antwoordt de route 404. Er wordt niets opgeslagen: je krijgt een voorstel terug dat je zelf met PUT .../schema of in POST /knowledge/graphs gebruikt.

curl -s -X POST "$CORE_URL/assist/graph-schema" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"description": "Leveranciers, de producten die ze leveren en de stoffen in elk product.",
         "collection_id": "col_2Pf7...", "sample_size": 3}'
Veld Wat het doet
description wat de graaf moet bevatten, in gewone taal
collection_id een collectie waarvan Core de meest recente bronnen die je sleutel bereikt als voorbeeld leest; elke bron wordt op 4.000 tekens afgekapt
sample_size hoeveel bronnen, 1 tot en met 5; standaard 5
current_schema een bestaand schema om aan te passen in plaats van opnieuw te beginnen

Minstens één van description en collection_id is verplicht. Het antwoord bevat schema (precies de body die PUT .../schema aanneemt), explanation, het model dat het schreef en sample: de bronnen die het model te zien kreeg. De tekst van die bronnen gaat naar het hulpmodel; dat is een modelaanroep onder de policy van je sleutel, en hij wordt gemeten als modeltokens.

3. Feiten zelf schrijven

curl -s -X POST "$CORE_URL/knowledge/graphs/$GRAPH_ID/entities" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"type": "Company", "name": "Acme", "aliases": ["Acme BV"],
         "properties": {"country": "NL"}}'

curl -s -X POST "$CORE_URL/knowledge/graphs/$GRAPH_ID/relations" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"type": "supplies", "from_entity_id": "kge_3Ta8...",
         "to_entity_id": "kge_7Lm2...", "properties": {"since": 2021}}'

Core controleert elk feit tegen de laatste schemaversie: het type moet bestaan, de eigenschappen moeten aan het JSON Schema van het type voldoen, een type met identity_keys heeft voor elke sleutel een waarde nodig, en de twee uiteinden van een relatie moeten in de graaf bestaan en van een type zijn dat het relatietype daar toestaat.

Aanroep Doet
GET .../entities entiteiten opsommen; type beperkt tot één type, q zoekt op naam en aliassen
GET .../relations relaties opsommen; type beperkt tot één type, entity_id tot de relaties aan één entiteit
GET .../entities/{id}, GET .../relations/{id} één feit, met origin, schema_version en sources
PATCH .../entities/{id} name, aliases of properties vervangen
PATCH .../relations/{id} properties vervangen
DELETE .../entities/{id} de entiteit verwijderen, met de relaties aan haar uiteinden
DELETE .../relations/{id} alleen de relatie verwijderen

Een feit dat je zelf schrijft heeft origin: manual. Het heeft geen bron, verdwijnt niet als een document verdwijnt en is voor iedere lezer van de graaf zichtbaar.

Een uitgelezen feit (origin: extracted) kun je niet wijzigen of verwijderen: PATCH en DELETE antwoorden 409, want de volgende keer dat zijn bronnen worden gelezen, zou het terugkomen. Verbeter de bron, of schrijf een handmatig feit. Vindt het uitlezen een entiteit die al handmatig bestaat, dan wordt de bron aan die entiteit gekoppeld en blijft ze handmatig, met haar eigen waarden.

4. Vullen uit bronnen

Een graaf leest feiten uit de bronnen van de collecties die je eraan koppelt, met een ingest-pipeline die je kiest:

curl -s -X PATCH "$CORE_URL/knowledge/graphs/$GRAPH_ID" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"collection_ids": [{"collection_id": "col_2Pf7...",
                            "metadata_filter": {"afdeling": "inkoop"}}],
         "ingest_pipeline": {"pipeline_id": "pip_...", "version": 1}}'
  • collection_ids: hoogstens 16 collecties, elk met een optioneel metadata_filter. Alleen bronnen waarvan de metadata het filter bevat, worden gelezen; zonder filter de hele collectie.
  • ingest_pipeline: een versie van een eigen pipeline (pipeline_id en version) of een ingebouwd sjabloon ({"template": "graph/..."}). De pipeline moet eindigen op de stap graph_store. Zonder pipeline leest de graaf geen enkele bron.
  • embedding_model: het embedding-model waarmee de namen van entiteiten worden ingebed, zodat een zoekopdracht ze ook op betekenis vindt. null gebruikt de standaard van de installatie.

De pipeline kiezen

Drie ingebouwde sjablonen eindigen op graph_store:

Sjabloon Wat het doet Model
graph/llm-extract laadt, leest en splitst een document; een chatmodel haalt uit elk fragment de entiteiten en relaties die het schema noemt ja, per fragment
graph/from-metadata maakt entiteiten uit de metadata van een bron, bijvoorbeeld de klant bij wie een map hoort nee
graph/from-tables maakt entiteiten en relaties uit elke rij van een tabel, bijvoorbeeld een leverancierslijst nee

De twee sjablonen zonder model kun je rechtstreeks noemen. Hun mapping is een voorbeeld: graph/from-metadata leest een entiteit Company uit de metadatasleutel company, en graph/from-tables leest Company, Product en supplies uit de kolommen supplier en product. Past dat niet bij je schema, maak dan een eigen pipeline met een eigen mapping.

graph/llm-extract kun je niet rechtstreeks noemen: de stap graph_extract moet een model noemen, en het sjabloon legt er geen vast. Maak een eigen pipeline met dezelfde stappen en het model ingevuld:

curl -s -X POST "$CORE_URL/knowledge/pipelines" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{
      "name": "leveranciers-uitlezen",
      "kind": "ingest",
      "config": {
        "nodes": [
          {"id": "input", "type": "input"},
          {"id": "load", "type": "load"},
          {"id": "parse", "type": "parse"},
          {"id": "split", "type": "sentence_splitter"},
          {"id": "extract", "type": "graph_extract", "config": {"model": "<een model-id>"}},
          {"id": "gstore", "type": "graph_store"},
          {"id": "output", "type": "output"}
        ],
        "edges": [
          {"source": "input", "target": "load"},
          {"source": "load", "target": "parse"},
          {"source": "parse", "target": "split"},
          {"source": "split", "target": "extract"},
          {"source": "extract", "target": "gstore"},
          {"source": "gstore", "target": "output"}
        ]
      }
    }'

Zet de pipeline die je terugkrijgt met zijn versie in ingest_pipeline. Een pipeline die niet op graph_store eindigt, of een graph_extract zonder model, geeft 422 op ingest_pipeline. Dezelfde pipeline kan niet tegelijk een index en een graaf vullen.

Een mapping, voor metadata_to_graph en rows_to_graph, is één JSON-object:

{
  "entities": [
    {"as": "leverancier", "type": "Company", "name": "supplier"},
    {"as": "product", "type": "Product", "name": "product", "properties": {"sku": "artikelnummer"}}
  ],
  "relations": [{"type": "supplies", "from": "leverancier", "to": "product"}]
}

name en elke eigenschap noemen de metadatasleutel of de kolom waaruit de waarde komt; een relatie verbindt twee entiteiten via hun as. Ontbreekt de naam van een entiteit in een bron, dan wordt die entiteit niet geschreven, en een relatie ernaartoe ook niet. Zie Pipelines voor het bewerken van stappen.

Elke stap houdt wat hij vindt aan het schema: een type dat het schema niet noemt, valt weg, net als een eigenschap die het type niet heeft of een waarde die niet aan het schema voldoet. Er komt dus niets in de graaf wat het schema niet toestaat.

Wanneer een bron wordt gelezen

Een nieuwe bron in een gekoppelde collectie wordt automatisch gelezen. Wijzig je de metadata van een bron, dan bepaalt Core opnieuw in welke grafen ze hoort. Bronnen die er al stonden toen je de collectie koppelde of de pipeline koos, worden pas gelezen als je opnieuw uitleest.

GET .../sources geeft per bron de status (queued, processing, ready of failed), het aantal feiten (fact_count) en bij een fout een vaste error_code. Een bron die niets noemt wat het schema kent, is ready met fact_count: 0. Een graaf zonder schema laat een bron mislukken met no_schema, een pipeline die inmiddels is verwijderd met no_pipeline, en een graaf leest een bron in zijn geheel, dus laat hij een bron groter dan de uploadlimiet (standaard 25 MiB) mislukken met too_large.

Wordt een bron verwijderd, dan verdwijnen de uitgelezen feiten die alleen op die bron steunden. Wat andere bronnen ook noemen, blijft.

Opnieuw uitlezen

curl -s -X POST "$CORE_URL/knowledge/graphs/$GRAPH_ID/reindex" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"mode": "full"}'

full (de standaard) leest elke bron binnen bereik opnieuw en haalt weg wat bronnen zeiden die de graaf niet meer leest. failed probeert alleen de bronnen opnieuw waarvan het laatste uitlezen mislukte. Het antwoord is 202 met een run; GET .../runs en GET .../runs/{run_id} tonen de voortgang (sources_total, sources_done, sources_failed). Loopt er al een run voor deze graaf of staat er een in de wachtrij, dan is het antwoord 409.

Lees opnieuw uit nadat je het schema, de pipeline of het embedding-model wijzigde, een collectie koppelde waar al bronnen in stonden, of een filter verruimde of versmalde.

5. De graaf bevragen

Een vraag is een formulier en geen querytekst. Je begint bij entiteiten van één type en volgt hoogstens drie relaties:

curl -s -X POST "$CORE_URL/knowledge/graphs/$GRAPH_ID/query" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{
      "start": {"type": "Company", "name": "Acme",
                "filters": [{"field": "country", "op": "eq", "value": "NL"}]},
      "follow": [
        {"relation": "supplies", "direction": "out", "to_type": "Product"}
      ],
      "limit": 50
    }'

Dit leest als: Nederlandse bedrijven met de naam Acme, en de producten die ze leveren.

Onderdeel Wat je invult
start.type het entiteitstype waar de vraag begint; verplicht
start.name of start.ids een naam, gezocht in namen en aliassen, of een lijst id's; niet allebei. Zonder een van beide begint de vraag bij elke entiteit van het type
start.filters filters op eigenschappen van de beginentiteiten
follow hoogstens drie stappen, in volgorde
follow[].relation het relatietype dat de stap volgt. Laat je het weg, dan volgt de stap elk type; dat is de duurste vorm
follow[].direction out (met de relatie mee, de standaard), in (ertegenin) of both
follow[].to_type het entiteitstype dat de stap moet bereiken
follow[].filters, follow[].relation_filters filters op de bereikte entiteit en op de relatie zelf
limit het maximum aantal paden, 1 tot en met 500; standaard 100

Een filter is {"field", "op", "value"} met eq, ne, lt, lte, gt, gte of contains; meerdere filters gelden samen. contains werkt alleen op tekst.

Het antwoord, hier ingekort tot één entiteit, één relatie en de velden die je meestal leest:

{
  "entities": [{"id": "kge_3Ta8...", "type": "Company", "name": "Acme", "properties": {"country": "NL"},
                "origin": "extracted", "sources": [{"id": "src_...", "collection_id": "col_...",
                "title": "Raamcontract Acme 2024"}]}],
  "relations": [{"id": "kgr_1Xc4...", "type": "supplies", "from_entity_id": "kge_3Ta8...",
                 "to_entity_id": "kge_7Lm2..."}],
  "paths": [["kge_3Ta8...", "kgr_1Xc4...", "kge_7Lm2..."]],
  "truncated": false
}

paths bevat elk pad als id's: de beginentiteit, en per stap een relatie en een entiteit. entities en relations bevatten elk feit op een pad één keer. sources noemt hoogstens vijf bronnen per feit, en alleen bronnen die de persoon van het verzoek mag lezen.

Grenzen. Een stap volgt hoogstens 50 relaties per entiteit en geeft hoogstens 200 entiteiten door aan de volgende stap. Heeft een grens of limit het antwoord afgekapt, dan is truncated true: het antwoord is dan onvolledig. Een vraag heeft een eigen tijdslimiet, standaard vijf seconden.

Antwoord Betekenis
422 met een veld, zoals follow[1].relation het schema kent het type, de relatie of de eigenschap niet, de richting past niet bij het relatietype, of de operator past niet bij de eigenschap
422 na een tijdsoverschrijding de vraag was te breed; begin bij minder entiteiten of noem het relatietype
409 de graaf heeft nog geen schema
403 de policy van je sleutel vereist een agent; deze route werkt buiten een beurt
404 de graaf bestaat niet of je sleutel bereikt hem niet

Een vraag via deze route wordt niet als verbruik gemeten.

6. Een model laten zoeken

Een model bereikt een graaf via een zoektool, net als een index. De zoektool draait een retrieval-pipeline met een bronblok graph. Drie sjablonen hebben er een:

Sjabloon Wat de tool doet
retrieval/graph zoekt in de graaf wat de vraag noemt en de feiten eromheen. Geen modelaanroep. Voor vragen over verbanden
retrieval/graph-documents het model vult zelf een vraag in zoals hierboven; daarna wordt het hele document achter elk gevonden feit gelezen uit zijn collectie
retrieval/hybrid-graph zoekt op betekenis in een index en in een graaf, en voegt passages en feiten samen in één rangschikking

Kopieer een sjabloon en vul de bronblokken in, registreer daarna de tool op die versie:

curl -s -X POST "$CORE_URL/knowledge/pipelines/from-template" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"template": "retrieval/hybrid-graph", "name": "leveranciers-zoeken",
         "sources": {"index": "idx_8Bd3...", "graph": "kg_5Wn1..."}}'

curl -s -X POST "$CORE_URL/knowledge/retrieval-tools" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"name": "leveranciers",
         "description": "Zoekt leveranciers, hun producten en de contracten daarover.",
         "pipeline": {"pipeline_id": "pip_...", "version": 1}}'

Het bronblok heet in deze sjablonen graph; retrieval/graph-documents heeft daarnaast een blok collection, en retrieval/hybrid-graph een blok index. Bied de tool aan als {"type": "tool", "name": "knowledge__leveranciers"}.

Wat het model terugkrijgt:

  • de gevonden entiteiten en relaties als gestructureerde gegevens, met voor een ingevulde vraag ook de paden en truncated;
  • per feit met een bron die de persoon mag lezen een passage met bronvermelding, zoals Acme -- supplies -> Widget (since: 2021). Een handmatig feit heeft geen bron en bereikt het model alleen als gegevens.

Een tool op retrieval/graph-documents biedt het model een argument graph_query aan, met de types en relaties van het schema als keuzes. Een vraag die het schema weigert, komt bij het model terug als ongeldige argumenten, zodat het de vraag kan verbeteren; de beurt mislukt daar niet door.

Een graaf wordt niet op metadata gefilterd. De filter_keys van een tool versmallen alleen wat er in indexen en collecties wordt gezocht. Een metadata_filter op een tool waarvan de pipeline een blok graph heeft, geeft 422. Wat een graaf bevat, bepaal je met de collecties die hij leest, hun filters en de zichtbaarheid.

Elke zoekstap over een graaf telt als één zoekeenheid (retrieval), plus embedding-tokens als er op betekenis is gezocht en modeltokens als een stap zelf een model aanroept.

Wie welke feiten ziet

De zichtbaarheid kies je bij het aanmaken en verandert daarna niet.

visibility Leest uit Een lezer ziet
shared alleen bronnen die van niemand persoonlijk zijn elk feit in de graaf
per_person elke bron die de koppelingen binnenhalen een feit als minstens één bron erachter voor die persoon zichtbaar is, en elk handmatig feit

Gebruik shared voor kennis van de organisatie zelf: een productcatalogus, een leveranciersregister. Bronnen van een persoon komen er nooit in, wat het filter ook zegt. Let op: binnen een gedeelde graaf versmallen de labels op bronnen niets. Een bron die een persoon via de regels over Kennisbronnen niet mag zien, draagt wel feiten bij die elke lezer van de graaf ziet. Moeten labels op bronnen ook binnen de graaf gelden, kies dan per_person.

Gebruik per_person voor documenten die van personen zijn: eigen contracten, uploads, notities. Een persoon ziet dan van een entiteit alleen de namen en eigenschappen die haar eigen zichtbare bronnen noemden. Een verzoek noemt de persoon met de header X-Core-Subject (Bouwen voor personen).

Welke grafen een sleutel bereikt, bepalen regels over Kennisgrafen in de policy, op naam of op label. De standaardregels staan elke graaf toe. Een graaf die geen regel toestaat, ontbreekt in lijsten en geeft 404; er een aanmaken onder een naam die geen regel toestaat, geeft 403.

Wat uitlezen kost

Uitlezen met een model kost modeltokens naar rato van de tekst: één aanroep per fragment, voor elke bron die de graaf leest, en opnieuw bij elke keer opnieuw uitlezen. Het model van de stap bepaalt de prijs, dus een goedkoper model kiezen doe je in de pipeline. De sjablonen zonder model kosten niets; het inbedden van nieuwe entiteiten kost embedding-tokens. Het verbruik telt mee tegen de kostenlimiet van de organisatie.

Wat een graaf niet doet

  • Entiteiten worden samengevoegd op identiteitssleutels of op genormaliseerde naam, niet op gelijkenis. Acme en Acme Holding zijn twee entiteiten, tenzij hun type identiteitssleutels heeft en beide dezelfde waarden dragen.
  • Een model kan tijdens een beurt geen feiten in een graaf schrijven.
  • Een graaf kan niet in een publicatie.
  • Een vraag kent geen paden van onbepaalde lengte en geen optellingen over een pad.