Blog

Agent Skills via de Claude API: een presentatie maken en downloaden

Markdown

Een kant-en-klare pptx-skill koppelen aan de Claude API: container.skills, de tool voor code-uitvoering, de reactie controleren en de presentatie downloaden via de Files API.

Lees het volledige artikel

Belangrijkste punt

Kant-en-klare Agent Skills voor documenten kunnen rechtstreeks via de Claude API worden gebruikt: vermeld de skill in container.skills en schakel code_execution in. Gebruik voor je eerste verzoek de combinatie van model en tool uit het officiële voorbeeld. Download het gegenereerde bestand apart met de bijbehorende file_id; in het voorbeeld wordt voor het verzoek en de download dezelfde API-sleutel gebruikt.

Meer instructies vind je in de Ailoot-documentatie.

Wat heb je nodig om Agent Skills via de Claude API te gebruiken?

Deze handleiding gaat over het rechtstreeks vanuit een programma aanroepen van de API, niet over het installeren van een skill in Claude Code. Je hebt een Claude API-sleutel, toegang tot het gekozen model en Python 3 nodig. De onderstaande code gebruikt de standaardbibliotheek van Python en HTTP, dus de versie van de SDK die je hebt geïnstalleerd maakt niet uit. De sleutel wordt doorgegeven via de omgevingsvariabele ANTHROPIC_API_KEY; zet deze niet in de programmatekst of in een gesprek.

De API biedt kant-en-klare Anthropic-skills: pptx voor presentaties, xlsx voor spreadsheets, docx voor Word-documenten en pdf voor pdf’s. Ze worden gekoppeld met type: "anthropic". Deze handleiding gebruikt een kant-en-klare skill en uploadt geen aangepaste skill.

De officiële quickstart, geraadpleegd bij de voorbereiding van dit materiaal, gebruikt de combinatie claude-opus-5-5 en code_execution_20260521. Dat is de combinatie die hieronder wordt gebruikt. Dit voorbeeld is gecontroleerd aan de hand van de documentatie; het is geen verslag van een betaalde API-aanroep.

Welke headers gebruik je voor Agent Skills en de Files API?

Het voorbeeld volgt het huidige HTTP-contract uit de quickstart: de headers zijn x-api-key, anthropic-version: 2023-06-01 en content-type: application/json; het verstuurt geen anthropic-beta. Combineer geen model, aanroepmethode en antwoordformaat uit verschillende generaties van de documentatie.

De documentatie van de Files API vermeldt ook dat de API niet meer in bèta is. De oude header files-api-2025-04-14 behoudt de oude antwoordformaten; als je de header verwijdert, worden de huidige formaten gebruikt. Daarom kun je een oud voorbeeld met client.beta.files niet zomaar combineren met een nieuw voorbeeld.

We migreren hier geen bestaande service van het oude beta-contract. Heb je zo’n service al, controleer dan eerst de headers en antwoordformaten. Het woord beta in de naam van een SDK-methode is op zichzelf niet voldoende: de documentatie van de Files API legt afzonderlijk uit hoe verschillende SDK-versies zich gedragen.

Hoe maak je een presentatie en download je het bestand?

Het voorbeeld verstuurt een opdracht om vijf dia’s over hernieuwbare energie te maken, slaat het volledige antwoord op in skill-response.json en haalt vervolgens de ID’s van de gegenereerde bestanden eruit. Het haalt geen opslagpad uit het antwoord van het model: de bestanden worden opgeslagen in een nieuwe lokale map met de naam skill-output. Als die map al bestaat, stopt het programma om te voorkomen dat het vorige resultaat wordt overschreven. De map wordt vóór de API-aanroep gemaakt en blijft dus ook na een mislukte aanroep bestaan. Bekijk en bewaar die poging voordat je opnieuw iets uitvoert; verwijder de map niet alleen om een herhaling te laten slagen.

import json
import os
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

API = "https://api.anthropic.com/v1"
KEY = os.environ["ANTHROPIC_API_KEY"]
HEADERS = {
    "x-api-key": KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
}

def request(path, payload=None, binary=False):
    data = None if payload is None else json.dumps(payload).encode("utf-8")
    req = Request(API + path, data=data, headers=HEADERS)
    try:
        with urlopen(req, timeout=600) as result:
            body = result.read()
    except HTTPError as error:
        try:
            details = json.loads(error.read().decode("utf-8"))
        except (ValueError, UnicodeError):
            details = {}
        info = details.get("error", {})
        request_id = error.headers.get("request-id") or details.get("request_id")
        diagnostic = {
            "http_status": error.code,
            "request_id": request_id,
            "error_type": info.get("type"),
            "message": str(info.get("message", "No JSON error message")).replace(KEY, "[REDACTED]")[:1000],
        }
        (out / "skill-error.json").write_text(
            json.dumps(diagnostic, ensure_ascii=False, indent=2), encoding="utf-8"
        )
        raise RuntimeError(
            f"Claude API HTTP {error.code}; request_id={request_id}; inspect skill-error.json"
        ) from None
    return body if binary else json.loads(body)

out = Path("skill-output")
out.mkdir(exist_ok=False)
response = request("/messages", {
    "model": "claude-opus-5-5",
    "max_tokens": 16000,
    "container": {
        "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]
    },
    "messages": [{
        "role": "user",
        "content": "Maak een presentatie over hernieuwbare energie met 5 dia’s in mijn voorkeurstaal.",
    }],
    "tools": [{"type": "code_execution_20260521", "name": "code_execution"}],
})
(out / "skill-response.json").write_text(
    json.dumps(response, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("stop_reason:", response.get("stop_reason"))

file_ids = []
for block in response["content"]:
    if block["type"] == "bash_code_execution_tool_result":
        result = block["content"]
        if result["type"] == "bash_code_execution_result":
            for item in result["content"]:
                file_id = item.get("file_id")
                if file_id and file_id not in file_ids:
                    file_ids.append(file_id)
if not file_ids:
    raise RuntimeError("No generated file_id; inspect skill-response.json")

for index, file_id in enumerate(file_ids, start=1):
    from urllib.parse import quote
    file_path = "/files/" + quote(file_id, safe="")
    metadata = request(file_path)
    if metadata.get("downloadable") is not True:
        raise RuntimeError("This file cannot be downloaded through Files API")
    target = out / f"result-{index}.pptx"
    target.write_bytes(request(file_path + "/content", binary=True))
    print("Saved:", target)

De namen result-1.pptx enzovoort zijn voor deze presentatietaak gekozen. Een bestandsextensie bewijst niet wat het bestand bevat: open het gedownloade document en controleer de indeling, het aantal dia’s en de tekst. Beschouw alleen een HTTP 200-status of de aanwezigheid van een file_id niet als een geslaagd resultaat.

Het programma geeft stop_reason, de reden waarom de reactie is gestopt, weer en slaat het volledige antwoord op om het te kunnen controleren. Het zet het gesprek niet voort en herhaalt het betaalde verzoek niet automatisch. Een gedownload bestand betekent niet dat de hele taak is afgerond: controleer het opgeslagen antwoord en het document zelf voordat je beslist of er nog een verzoek nodig is.

Bekijk na een mislukte poging skill-error.json als dat bestand bestaat. Het bevat de HTTP-status, een eventueel door de aanbieder geleverde verzoek-ID en een begrensde foutmelding waaruit de ingestelde API-sleutel is verwijderd. Controleer de melding op andere privégegevens voordat je haar deelt. Vergelijk authenticatiefouten, rechten- en modelfouten, factureringslimieten en fouten in de verzoekindeling met de officiële foutreferentie; niet elke fout vraagt om een nieuwe betaalde generatie.

Staan er al gegenereerde file_id-waarden in skill-response.json en is alleen een download mislukt, hervat dan die download met de bekende ID’s in plaats van de presentatie opnieuw te genereren. Is de uitkomst van de generatie onbekend, zoek dan eerst uit wat er met de oorspronkelijke poging is gebeurd. Een FileExistsError voor skill-output is een lokaal mapconflict, geen API-fout. Kies pas voor een gemotiveerde nieuwe generatie bewust een nieuwe, ongebruikte uitvoermap; behoud exist_ok=False en overschrijf de eerdere poging niet. Een andere map voorkomt geen dubbele generatie of dubbele kosten.

Welke bestandsrechten zijn nodig

De Files API koppelt bestanden aan een workspace, niet aan een afzonderlijke gebruiker van je applicatie of aan een gesprek. Volgens de documentatie kan elke API-sleutel met toegang tot die workspace de bestanden daarin benaderen. In het voorbeeld wordt dezelfde sleutel gebruikt om het bestand te genereren en te downloaden. Accepteer nooit een file_id van eindgebruikers of andere niet-vertrouwde bronnen. Bewaar deze ID’s op de server en houd in je eigen applicatie bij welke bestanden bij welke gebruikers horen.

Controleer de bestandsmetadata afzonderlijk: daarin moet downloadable: true staan. Volgens de documentatie kun je bestanden downloaden die door skills of de tool voor code-uitvoering zijn gegenereerd; bestanden die je zelf uploadt, hebben downloadable: false en een poging om ze te downloaden geeft een 400-fout. Toegang tot de workspace en toestemming om een specifiek bestand te downloaden zijn afzonderlijke vereisten.

Voor een applicatie met meerdere gebruikers beveelt de documentatie van de Files API een aparte workspace per klant aan als grens voor bestandsisolatie. Deze aanbeveling betreft specifiek bestanden; pas haar niet toe op alle entiteiten en rechten zonder de relevante API te controleren.

Kant-en-klare skills en aangepaste skills

De ID’s van kant-en-klare skills zijn pptx, xlsx, docx en pdf. In de referentie voor de skillslijst wordt de bron custom beschreven als privé voor de workspace, terwijl anthropic gedeeld en alleen-lezen is.

version: "latest" in het eerste voorbeeld betekent de nieuwste gepubliceerde versie van de skill. Houd er bij een reproduceerbare integratie rekening mee dat deze selectie kan veranderen; om over te stappen op een vaste versie moet je een bestaande versie kiezen van de skill die je gebruikt, en geen ID uit een voorbeeld verzinnen.

Beperkingen en de volgende stap

Het voorbeeld behandelt alleen het maken van een presentatie met een kant-en-klare skill en het downloaden ervan. Het uploaden en verwijderen van aangepaste skills en toegang tot diensten van derden zijn hier niet getest. Voor die taken moet je de relevante API afzonderlijk controleren.

Als je taak betrekking heeft op bestanden in de Claude-interface, is er een aparte handleiding over hoe je een bestand naar Claude uploadt. Voor rechtstreekse aanroepen, zie hoe je het API-tokengebruik berekent. Voor werk in een terminalagent, lees hoe je Claude Code-skills kiest: die worden op een andere manier gekoppeld.

Voor de eerste controle is één kant-en-klare pptx-skill voldoende: controleer de reactie, download een bestand dat mag worden gedownload en open het. Voeg aangepaste skills en toegangsisolatie tussen klanten daarna toe.

Maak kennis met Ailoot wanneer je er klaar voor bent.

Meer details

Vragen en antwoorden

Waarom volstaat een file_id niet om de presentatie afgerond te noemen?

De ID laat zien dat een bestand is teruggegeven, niet dat het een bruikbare presentatie is. Controleer of de metadata downloadable: true bevat, download met toegang tot de werkruimte en open het bestand om formaat, aantal dia’s en tekst te controleren.

Kan ik elk bestand downloaden dat naar de Claude Files API is geüpload?

Nee. De gids onderscheidt gegenereerde bestanden met downloadable: true van geüploade bestanden met downloadable: false. Werkruimtetoegang en toestemming om een bepaald bestand te downloaden zijn afzonderlijke controles; accepteer geen willekeurige bestands-ID’s van eindgebruikers.

Alle artikelen