Aan de slag met multimodale vision-chat-apps met behulp van Azure OpenAI

In dit artikel leest u hoe u Azure Multimodale OpenAI-modellen gebruikt om antwoorden te genereren op gebruikersberichten en geüploade afbeeldingen in een chat-app. Dit voorbeeld van een chat-app bevat ook alle infrastructuur en configuratie die nodig zijn om Azure OpenAI-resources in te richten en de app te implementeren in Azure Container Apps met behulp van de Azure Developer CLI.

Door de instructies in dit artikel te volgen, gaat u het volgende doen:

  • Implementeer een Azure Container-chat-app die gebruikmaakt van beheerde identiteit voor verificatie.
  • Upload afbeeldingen die moeten worden gebruikt als onderdeel van de chatstream.
  • Chat met een Azure OpenAI multimodal Large Language Model (LLM) met behulp van de Antwoorden-API van de OpenAI-bibliotheek.

Zodra u dit artikel hebt voltooid, kunt u beginnen met het wijzigen van het nieuwe project met uw aangepaste code.

Notitie

In dit artikel worden een of meer AI-app-sjablonen gebruikt als basis voor de voorbeelden en richtlijnen in het artikel. AI-app-sjablonen bieden u een goed onderhouden, eenvoudig te implementeren referentie-implementaties die u helpen een startpunt van hoge kwaliteit voor uw AI-apps te garanderen.

Architectuuroverzicht

In het volgende diagram ziet u een eenvoudige architectuur van de chat-app: Diagram met architectuur van client-naar back-end-app.

De chat-app wordt uitgevoerd als een Azure container-app. De app gebruikt beheerde identiteit via Microsoft Entra ID om te verifiëren met Azure OpenAI in productie, in plaats van een API-sleutel. Tijdens de ontwikkeling ondersteunt de app meerdere verificatiemethoden, waaronder Azure Cli-referenties en API-sleutels voor ontwikkelaars.

De toepassingsarchitectuur is afhankelijk van de volgende services en onderdelen:

  • Azure OpenAI vertegenwoordigt de AI-provider waarnaar we de query's van de gebruiker verzenden.
  • Azure Container Apps is de containeromgeving waarin de toepassing wordt gehost.
  • Managed Identity helpt ons om beveiliging van de beste klasse te garanderen en elimineert de vereiste voor u als ontwikkelaar om een geheim veilig te beheren.
  • Bicep-bestanden voor het inrichten van Azure resources, waaronder Azure OpenAI, Azure Container Apps, Azure Container Registry, Azure Log Analytics en RBAC-rollen (op rollen gebaseerd toegangsbeheer).
  • Een Python Quart-app die gebruikmaakt van het openai-pakket voor het genereren van antwoorden op gebruikersberichten met geüploade afbeeldingsbestanden.
  • Een eenvoudige HTML-/JavaScript-front-end die reacties van de back-end streamt met behulp van JSON-lijnen via een ReadableStream.

Kosten

In een poging om de prijzen in dit voorbeeld zo laag mogelijk te houden, maken de meeste resources gebruik van een basis- of verbruiksprijscategorie. Wijzig uw niveau indien gewenst op basis van het beoogde gebruik. Om kosten te vermijden, verwijdert u de resources wanneer u klaar bent met het artikel.

Meer informatie over kosten in de voorbeeldopslagplaats.

Vereisten

Er is een ontwikkelcontaineromgeving beschikbaar met alle afhankelijkheden die nodig zijn om dit artikel te voltooien. U kunt de ontwikkelcontainer uitvoeren in GitHub Codespaces (in een browser) of lokaal met behulp van Visual Studio Code.

Als u dit artikel wilt gebruiken, moet u voldoen aan de volgende vereisten:

Open ontwikkelomgeving

Gebruik de volgende instructies om een vooraf geconfigureerde ontwikkelomgeving met alle vereiste afhankelijkheden te implementeren om dit artikel te voltooien.

GitHub Codespaces voert een ontwikkelingscontainer uit die wordt beheerd door GitHub met Visual Studio Code voor het web als de gebruikersinterface. Gebruik voor de eenvoudigste ontwikkelomgeving GitHub Codespaces zodat u de juiste ontwikkelhulpprogramma's en afhankelijkheden vooraf hebt geïnstalleerd om dit artikel te voltooien.

Belangrijk

Alle GitHub-accounts kunnen Codespaces elke maand maximaal 60 uur gratis gebruiken met twee kernexemplaren. Zie GitHub Codespaces maandelijks opgenomen opslag- en kernuren voor meer informatie.

Gebruik de volgende stappen om een nieuwe GitHub Codespace te maken op de tak main van de Azure-Samples/openai-chat-vision-quickstart GitHub-opslagplaats.

  1. Klik met de rechtermuisknop op de volgende knop en selecteer Koppeling openen in een nieuw venster. Met deze actie kunt u beschikken over de ontwikkelomgeving en de documentatie die ter beoordeling beschikbaar is.

    Open in GitHub Codespaces

  2. Controleer op de codespace aanmaken pagina en selecteer Nieuwe codespace aanmaken

  3. Wacht tot de coderuimte opgestart is. Dit opstartproces kan enkele minuten duren.

  4. Meld u aan bij Azure met de Azure Developer CLI in de terminal onderaan het scherm.

    azd auth login
    
  5. Kopieer de code uit de terminal en plak deze in een browser. Volg de instructies voor verificatie met uw Azure-account.

De resterende taken in dit artikel vinden plaats in de context van deze ontwikkelingscontainer.

Implementeren en uitvoeren

De voorbeeldopslagplaats bevat alle code- en configuratiebestanden voor de chat-app Azure implementatie. Met de volgende stappen doorloopt u de voorbeeld-chat-app Azure implementatieproces.

Chat-app implementeren in Azure

Belangrijk

Als u kosten laag wilt houden, gebruikt dit voorbeeld de prijscategorieën basis- of verbruiksprijzen voor de meeste resources. Pas de laag zo nodig aan en verwijder resources wanneer u klaar bent om kosten te voorkomen.

  1. Voer de volgende Azure Developer CLI-opdracht uit voor het inrichten van Azure resources en de implementatie van broncode:

    azd up
    
  2. Gebruik de volgende tabel om de prompts te beantwoorden:

    Prompt Antwoord
    Naam van de omgeving Houd het kort en kleine letters. Voeg uw naam of alias toe. Bijvoorbeeld: chat-vision. Het wordt gebruikt als onderdeel van de naam van de resourcegroep.
    Abonnement Selecteer het abonnement waarin u de resources wilt maken.
    Locatie (voor hosting) Selecteer een locatie bij u in de buurt in de lijst.
    Locatie voor het Azure OpenAI-model Selecteer een locatie bij u in de buurt in de lijst. Als dezelfde locatie beschikbaar is als uw eerste locatie, selecteert u die.
  3. Wacht totdat de app is geïmplementeerd. De implementatie duurt meestal tussen 5 en 10 minuten.

Chat-app gebruiken om vragen te stellen aan het grote taalmodel

  1. In de terminal wordt een URL weergegeven na een geslaagde implementatie van de toepassing.

  2. Selecteer die URL die is gelabeld Deploying service web om de chattoepassing in een browser te openen.

    Schermopname van een geüploade afbeelding, een vraag over de afbeelding, het antwoord van de AI en het tekstvak.

  3. Upload een afbeelding in de browser door op Bestand kiezen te klikken en een afbeelding te selecteren.

  4. Stel een vraag over de geüploade afbeelding, zoals 'Waar gaat de afbeelding over?'.

  5. Het antwoord is afkomstig van Azure OpenAI en het resultaat wordt weergegeven.

De voorbeeldcode verkennen

In dit voorbeeld wordt een Azure Multimodale OpenAI-model gebruikt om antwoorden te genereren op gebruikersberichten en geüploade afbeeldingen.

Base64-codering van de geüploade afbeelding in de front-end

De geüploade afbeelding moet Base64 worden gecodeerd, zodat deze rechtstreeks als gegevens-URI kan worden gebruikt als onderdeel van het bericht.

In het voorbeeld verwerkt het volgende front-endcodefragment in de scripttag van het src/quartapp/templates/index.html bestand die functionaliteit. De toBase64 pijlfunctie gebruikt de readAsDataURL methode van deFileReader om asynchroon te lezen in het geüploade afbeeldingsbestand als een met base64 gecodeerde tekenreeks.

    const toBase64 = file => new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
    });

De toBase64 functie wordt aangeroepen door een luisteraar bij de gebeurtenis van het formulier submit.

De submit event handler verwerkt de volledige chats-interactiestroom. Wanneer de gebruiker een bericht verzendt, vindt de volgende stroom plaats:

  1. Hiermee wordt het geüploade afbeeldingsbestand (indien aanwezig) opgehaald en gecodeerd als Base64
  2. Maakt en geeft het bericht van de gebruiker weer in de chat, inclusief de geüploade afbeelding
  3. Bereidt een berichtencontainer voor een assistent voor met een "Typen..." indicator.
  4. Voegt het bericht van de gebruiker toe aan de berichtgeschiedenismatrix in de API-indeling voor antwoorden
  5. Verzendt een fetch POST-aanvraag naar het /chat/stream eindpunt met de berichtgeschiedenis en context (inclusief de met Base64 gecodeerde afbeelding en bestandsnaam)
  6. Verwerkt het gestreamde JSON-lijnen-antwoord om elke tekst-delta incrementeel weer te geven
  7. Verwerkt eventuele fouten tijdens het streamen
  8. Hiermee voegt u een knop spraakuitvoer toe nadat het volledige antwoord is ontvangen, zodat gebruikers het antwoord kunnen horen
  9. Hiermee wis je het invoerveld en herstelt de focus voor het volgende bericht.
form.addEventListener("submit", async function(e) {
    e.preventDefault();

    // Hide the no-messages-heading when a message is added
    document.getElementById("no-messages-heading").style.display = "none";

    const file = document.getElementById("file").files[0];
    const fileData = file ? await toBase64(file) : null;

    const message = messageInput.value;

    const userTemplateClone = userTemplate.content.cloneNode(true);
    userTemplateClone.querySelector(".message-content").innerText = message;
    if (file) {
        const img = document.createElement("img");
        img.src = fileData;
        userTemplateClone.querySelector(".message-file").appendChild(img);
    }
    targetContainer.appendChild(userTemplateClone);

    const assistantTemplateClone = assistantTemplate.content.cloneNode(true);
    let messageDiv = assistantTemplateClone.querySelector(".message-content");
    targetContainer.appendChild(assistantTemplateClone);

    messages.push({
        "role": "user",
        "content": [{"type": "input_text", "text": message}]
    });

    try {
        messageDiv.scrollIntoView();
        const response = await fetch("/chat/stream", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({
                messages: messages,
                context: {
                    file: fileData,
                    file_name: file ? file.name : null
                }
            })
        });

        if (!response.ok || !response.body) {
            throw new Error(`Request failed (${response.status})`);
        }

        let answer = "";
        for await (const chunk of readNDJSONStream(response.body)) {
            if (chunk.type === "error" || chunk.type === "response.failed") {
                messageDiv.innerHTML = "Error: " + (chunk.error || "Unknown error");
                break;
            }
            if (chunk.type === "response.output_text.delta") {
                // Clear out the DIV if its the first answer chunk we've received
                if (answer == "") {
                    messageDiv.innerHTML = "";
                }
                answer += chunk.delta;
                messageDiv.innerHTML = converter.makeHtml(answer);
                messageDiv.scrollIntoView();
            }
        }

        messages.push({
            "role": "assistant",
            "content": [{"type": "output_text", "text": answer}]
        });

        messageInput.value = "";

        const speechOutput = document.createElement("speech-output-button");
        speechOutput.setAttribute("text", answer);
        messageDiv.appendChild(speechOutput);
        messageInput.focus();
    } catch (error) {
        messageDiv.innerHTML = "Error: " + error;
    }
});

De afbeelding verwerken met de backend

In het src\quartapp\chat.py bestand wordt de back-endcode voor het verwerken van afbeeldingen gestart na het configureren van sleutelloze verificatie.

Notitie

Voor meer informatie over het gebruik van sleutelloze verbindingen voor verificatie en autorisatie voor Azure OpenAI raadpleegt u het artikel Get gestart met de bouwsteen Azure OpenAI-beveiliging Microsoft Learn.

Verificatieconfiguratie

De configure_openai() functie stelt de OpenAI-client in voordat de app aanvragen gaat verwerken. Het maakt gebruik van de decorator van @bp.before_app_serving Quart om verificatie te configureren op basis van omgevingsvariabelen. Met dit flexibele systeem kunnen ontwikkelaars in verschillende contexten werken zonder code te wijzigen.

Uitleg van verificatiemodi
  • Lokale ontwikkeling (OPENAI_HOST=local): Maakt verbinding met een lokale OpenAI-compatibele API-service (zoals Ollama of LocalAI) zonder verificatie. Gebruik deze modus voor het testen zonder internet- of API-kosten.
  • Azure OpenAI met API-sleutel (AZURE_OPENAI_KEY_FOR_CHATVISION omgevingsvariabele): maakt gebruik van een API-sleutel voor verificatie. Vermijd deze modus in productie omdat API-sleutels handmatige rotatie vereisen en beveiligingsrisico's vormen als ze worden blootgesteld. Gebruik dit voor lokale tests in een Docker-container zonder Azure CLI referenties.
  • Production with Managed Identity (RUNNING_IN_PRODUCTION=true): maakt gebruik van ManagedIdentityCredential voor verificatie met Azure OpenAI via de beheerde identiteit van de container-app. Deze methode wordt aanbevolen voor productie omdat hiermee de noodzaak voor het beheren van geheimen wordt verwijderd. Automatisch worden in Azure Container Apps de beheerde identiteit opgegeven en machtigingen verleend tijdens de implementatie via Bicep.
  • Ontwikkeling met Azure CLI (standaardmodus): gebruikt AzureDeveloperCliCredential om te verifiëren met Azure OpenAI met behulp van lokaal aangemelde Azure CLI referenties. Deze modus vereenvoudigt lokale ontwikkeling zonder API-sleutels te beheren.
Belangrijke implementatiedetails
  • De functie get_bearer_token_provider() vernieuwt Azure referenties en gebruikt deze als bearertokens.
  • Het pad van het Azure OpenAI-eindpunt, eindigt met /openai/v1/, het algemeen beschikbare OpenAI-compatibele eindpunt voor Microsoft Foundry-modellen.
  • De functie is asynchroon, omdat Quart een asynchroon web-app-framework is. Met Quart kunnen aanvraaghandlers asynchroon zijn, dus terwijl de app wacht op trage LLM API-antwoorden, kan de server andere aanvragen blijven verwerken.

Hier volgt de volledige verificatie-instellingscode van chat.py:

@bp.before_app_serving
async def configure_openai():
    bp.model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
    openai_host = os.getenv("OPENAI_HOST", "azure")

    if openai_host == "local":
        bp.openai_client = AsyncOpenAI(api_key="no-key-required", base_url=os.getenv("LOCAL_OPENAI_ENDPOINT"))
        current_app.logger.info("Using local OpenAI-compatible API service with no key")
    elif os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"):
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"),
        )
        current_app.logger.info("Using Azure OpenAI with key")
    elif os.getenv("RUNNING_IN_PRODUCTION"):
        client_id = os.environ["AZURE_CLIENT_ID"]
        azure_credential = ManagedIdentityCredential(client_id=client_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with managed identity credential for client ID %s", client_id)
    else:
        tenant_id = os.environ["AZURE_TENANT_ID"]
        azure_credential = AzureDeveloperCliCredential(tenant_id=tenant_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with az CLI credential for tenant ID: %s", tenant_id)

Chat-handlerfunctie

De chat_handler() functie verwerkt chataanvragen die naar het /chat/stream eindpunt worden verzonden. Er wordt een POST-aanvraag met een JSON-nettolading ontvangen.

De JSON-nettolading bevat:

  • berichten: Een lijst met gespreksgeschiedenis. Elk bericht heeft een role ('gebruiker' of 'assistent') en content (een matrix met inhoudsonderdelen met behulp van de invoerindeling van de Antwoorden-API).
  • context: Extra gegevens voor verwerking, waaronder:
    • bestand: Met Base64 gecodeerde afbeeldingsgegevens (bijvoorbeeld data:image/png;base64,...).
    • file_name: de oorspronkelijke bestandsnaam van de geüploade afbeelding (handig voor het vastleggen of identificeren van het afbeeldingstype).

De handler extraheert de berichtgeschiedenis en afbeeldingsgegevens. Als er geen afbeelding wordt geüpload, is nullde waarde van de afbeelding en de code verwerkt dit geval.

@bp.post("/chat/stream")
async def chat_handler():
    request_json = await request.get_json()
    request_messages = request_json["messages"]
    # Get the base64 encoded image from the request context
    # This will be None if no image was uploaded
    image = request_json["context"]["file"]

De invoermatrix bouwen voor vision-aanvragen

De functie response_stream() bereidt de invoermatrix voor die wordt verzonden naar de Azure OpenAI-antwoorden-API. De @stream_with_context decorator houdt de aanvraagcontext bij terwijl het antwoord wordt gestreamd.

Invoervoorbereidingslogica

  1. Begin met gespreksgeschiedenis: De functie begint met all_input, met alle vorige berichten, behalve de meest recente (request_messages[0:-1]). Berichten hebben al de indeling van de Responses-API vanuit de frontend.
  2. Het huidige gebruikersbericht verwerken op basis van aanwezigheid van afbeeldingen:
    • Met afbeelding: Voeg een input_image inhoudsonderdeel toe aan de bestaande inhoudsmatrix van de gebruiker.
    • Zonder afbeelding: voeg het bericht van de gebruiker toe as-is.
    @stream_with_context
    async def response_stream():
        # This sends all messages, so API request may exceed token limits
        all_input = list(request_messages[0:-1])

        # Add the current user message, appending image if provided
        if image:
            user_content = request_messages[-1]["content"] + [{"type": "input_image", "image_url": image}]
            all_input.append({"role": "user", "content": user_content})
        else:
            all_input.append(request_messages[-1])

Vervolgens roept bp.openai_client.responses.create de Azure OpenAI-antwoorden-API aan en wordt het antwoord gestreamd. Met store=False de parameter wordt de API geïnstrueerd om geen antwoorden op de server op te slaan, waardoor de aanroep staatloos wordt.

        openai_stream = await bp.openai_client.responses.create(
            model=bp.model_name,
            input=all_input,
            stream=True,
            temperature=0.3,
            store=False,
        )

Ten slotte wordt het antwoord teruggestreamd naar de client. De Antwoorden-API verzendt veel gebeurtenistypen, maar alleen de response.output_text.delta gebeurtenis is nodig voor het streamen van gegenereerde tekst. Foutgebeurtenissen worden geregistreerd en doorgestuurd naar de front-end.

        try:
            async for event in openai_stream:
                if event.type == "response.output_text.delta":
                    yield json.dumps({"type": event.type, "delta": event.delta}, ensure_ascii=False) + "\n"
                elif event.type in ("response.failed", "error"):
                    current_app.logger.error("Responses API error: %s", event)
                    yield json.dumps({"type": event.type}, ensure_ascii=False) + "\n"
        except Exception as e:
            current_app.logger.exception("Error in response stream")
            yield json.dumps({"error": str(e)}, ensure_ascii=False) + "\n"

    return Response(response_stream())

Front-endbibliotheken en -functionaliteiten

De front-end maakt gebruik van moderne browser-API's en bibliotheken om een interactieve chatervaring te maken. Ontwikkelaars kunnen de interface aanpassen of functies toevoegen door inzicht te krijgen in deze onderdelen:

  1. Spraakinvoer/-uitvoer: aangepaste webonderdelen maken gebruik van de Spraak-API's van de browser:

    • <speech-input-button>: converteert spraak naar tekst met behulp van de Web Speech-API' s SpeechRecognition. Het biedt een microfoonknop die luistert naar spraakinvoer en een speech-input-result gebeurtenis verzendt met de getranscribeerde tekst.

    • <speech-output-button>: leest tekst hardop voor met behulp van de SpeechSynthesis API. Deze wordt weergegeven na elk antwoord van de assistent met een luidsprekerpictogram, zodat gebruikers het antwoord kunnen horen.

    Waarom zou u browser-API’s gebruiken in plaats van Azure Speech Services?

    • Geen kosten - wordt volledig uitgevoerd in de browser
    • Directe reactie - geen netwerklatentie
    • Privacy: spraakgegevens blijven op het apparaat van de gebruiker
    • Geen extra Azure resources nodig

    Deze onderdelen bevinden zich in src/quartapp/static/speech-input.js en speech-output.js.

  2. Afbeeldingsvoorbeeld: Geeft de geüploade afbeelding weer in de chat voordat de analyse ter bevestiging wordt verzonden. De preview wordt automatisch bijgewerkt wanneer een bestand wordt geselecteerd.

    fileInput.addEventListener("change", async function() {
        const file = fileInput.files[0];
        if (file) {
            const fileData = await toBase64(file);
            imagePreview.src = fileData;
            imagePreview.style.display = "block";
        }
    });
    
  3. Bootstrap 5- en Bootstrap-pictogrammen: biedt responsieve UI-onderdelen en -pictogrammen. De app maakt gebruik van het Cosmo-thema van Bootswatch voor een modern uiterlijk.

  4. Op sjabloon gebaseerde berichtweergave: maakt gebruik van HTML-elementen <template> voor herbruikbare berichtindelingen, waardoor consistente stijl en structuur worden gegarandeerd.

Andere voorbeeldbronnen om te verkennen

Naast het voorbeeld van de chat-app zijn er andere bronnen in de opslagplaats om te verkennen voor meer informatie. Bekijk de volgende notebooks in de notebooks directory:

Laptop Beschrijving
chat_pdf_images.ipynb In dit notebook ziet u hoe u PDF-pagina's converteert naar afbeeldingen en verzendt naar een vision-model voor deductie.
chat_vision.ipynb Dit notebook is beschikbaar voor handmatig experimenteren met het vision-model dat in de app wordt gebruikt.

Gelokaliseerde inhoud: Spaanse versies van de notebooks bevinden zich in de notebooks/Spanish/ directory, die dezelfde praktische training bieden voor Spaans sprekende ontwikkelaars. Zowel Engelse als Spaanse notitieblokken tonen:

  • Vision-modellen rechtstreeks aanroepen voor experimenten
  • PDF-pagina's converteren naar afbeeldingen voor analyse
  • Parameters en testprompts aanpassen

Resources opschonen

Het opschonen van Azure-resources

De Azure resources die in dit artikel zijn gemaakt, worden in rekening gebracht voor uw Azure-abonnement. Als u deze resources in de toekomst niet meer nodig hebt, verwijdert u deze om te voorkomen dat er meer kosten in rekening worden gebracht.

Als u de Azure resources wilt verwijderen en de broncode wilt verwijderen, voert u de volgende Azure Developer CLI-opdracht uit:

azd down --purge

GitHub Codespaces opschonen

Als u de GitHub Codespaces-omgeving verwijdert, zorgt u ervoor dat u de hoeveelheid gratis rechten per kernuren kunt maximaliseren die u voor uw account krijgt.

Belangrijk

Zie GitHub Codespaces maandelijks inbegrepen opslag- en kernuren voor meer informatie over de rechten van uw GitHub-account.

  1. Meld u aan bij het dashboard GitHub Codespaces.

  2. Zoek de momenteel actieve Codespaces die uit de GitHub-opslagplaats Azure-Samples//openai-chat-vision-quickstart afkomstig zijn.

  3. Open het contextmenu voor de coderuimte en selecteer Verwijderen.

Hulp vragen

Meld uw probleem aan bij de Issues van de opslagplaats.

Volgende stappen