Cloud Run mit Gemini-Funktionsaufrufen verwenden

1. Einführung

Übersicht

In diesem Codelab erfahren Sie, wie Sie Gemini mithilfe einer neuen Funktion namens Funktionsaufrufe Zugriff auf Echtzeitdaten geben. Um Echtzeitdaten zu simulieren, erstellen Sie einen Wetterdienst-Endpunkt, der das aktuelle Wetter für zwei Standorte zurückgibt. Anschließend erstellen Sie eine Chat-App auf Basis von Gemini, die mithilfe von Funktionsaufrufen das aktuelle Wetter abruft.

Sehen wir uns die Funktionsaufrufe kurz an.

  • Der Prompt fragt nach aktuellen Wetterstandorten an einem bestimmten Standort.
  • Dieser Prompt und der Funktionsvertrag für getWeather() werden an Gemini gesendet
  • Gemini bittet darum, dass die Chatbot-App „getWeather(Seattle)“ aufruft in seinem Namen
  • Die App sendet die Ergebnisse (40 Grad F und Regen)
  • Gemini sendet die Ergebnisse an den Anrufer

Zur Erinnerung: Gemini ruft die Funktion nicht auf. Sie als Entwickler müssen die Funktion aufrufen und die Ergebnisse an Gemini zurücksenden.

Diagramm des Ablaufs für Funktionsaufrufe

Aufgaben in diesem Lab

  • So funktionieren Gemini-Funktionsaufrufe
  • Eine Gemini-basierte Chatbot-App als Cloud Run-Dienst bereitstellen

2. Einrichtung und Anforderungen

Voraussetzungen

Cloud Shell aktivieren

  1. Klicken Sie in der Cloud Console auf Cloud Shell aktivieren d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Wenn Sie Cloud Shell zum ersten Mal starten, wird ein Zwischenbildschirm mit einer Beschreibung der Funktion angezeigt. Wenn ein Zwischenbildschirm angezeigt wird, klicken Sie auf Weiter.

d95252b003979716.png

Die Bereitstellung und Verbindung mit Cloud Shell dauert nur einen Moment.

7833d5e1c5d18f54.png

Diese virtuelle Maschine verfügt über alle erforderlichen Entwicklertools. Es bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und wird in Google Cloud ausgeführt. Dadurch werden die Netzwerkleistung und die Authentifizierung erheblich verbessert. Viele, wenn nicht sogar alle Arbeiten in diesem Codelab können mit einem Browser erledigt werden.

Sobald Sie mit Cloud Shell verbunden sind, sollten Sie sehen, dass Sie authentifiziert sind und das Projekt auf Ihre Projekt-ID eingestellt ist.

  1. Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:
gcloud auth list

Befehlsausgabe

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob der gcloud-Befehl Ihr Projekt kennt:
gcloud config list project

Befehlsausgabe

[core]
project = <PROJECT_ID>

Ist dies nicht der Fall, können Sie die Einstellung mit diesem Befehl vornehmen:

gcloud config set project <PROJECT_ID>

Befehlsausgabe

Updated property [core/project].

3. Umgebungsvariablen einrichten und APIs aktivieren

Umgebungsvariablen einrichten

Sie können Umgebungsvariablen festlegen, die in diesem Codelab verwendet werden.

PROJECT_ID=<YOUR_PROJECT_ID>
REGION=<YOUR_REGION, e.g. us-central1>
WEATHER_SERVICE=weatherservice
FRONTEND=frontend
SERVICE_ACCOUNT="vertex-ai-caller"
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

APIs aktivieren

Bevor Sie dieses Codelab verwenden können, müssen Sie mehrere APIs aktivieren. Für dieses Codelab müssen die folgenden APIs verwendet werden. Sie können diese APIs aktivieren, indem Sie den folgenden Befehl ausführen:

gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    aiplatform.googleapis.com

4. Dienstkonto zum Aufrufen von Vertex AI erstellen

Dieses Dienstkonto wird von Cloud Run zum Aufrufen der Vertex AI Gemini API verwendet.

Erstellen Sie zuerst das Dienstkonto, indem Sie den folgenden Befehl ausführen:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Cloud Run to access Vertex AI APIs"

Weisen Sie dann dem Dienstkonto die Rolle „Vertex AI User“ zu.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
  --role=roles/aiplatform.user

5. Cloud Run-Back-End-Dienst erstellen

Erstellen Sie zunächst ein Verzeichnis für den Quellcode und speichern Sie das Verzeichnis mit cd.

mkdir -p gemini-function-calling/weatherservice gemini-function-calling/frontend && cd gemini-function-calling/weatherservice

Erstellen Sie dann eine package.json-Datei mit folgendem Inhalt:

{
    "name": "weatherservice",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "^4.18.3"
    }
}

Erstellen Sie als Nächstes eine app.js-Quelldatei mit folgendem Inhalt. Diese Datei enthält den Einstiegspunkt für den Dienst und die Hauptlogik für die Anwendung.

const express = require("express");
const app = express();

app.get("/getweather", (req, res) => {
    const location = req.query.location;
    let temp, conditions;

    if (location == "New Orleans") {
        temp = 99;
        conditions = "hot and humid";
    } else if (location == "Seattle") {
        temp = 40;
        conditions = "rainy and overcast";
    } else {
        res.status(400).send("there is no data for the requested location");
    }

    res.json({
        weather: temp,
        location: location,
        conditions: conditions
    });
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
    console.log(`weather service: listening on port ${port}`);
});

app.get("/", (req, res) => {
    res.send("welcome to hard-coded weather!");
});

Wetterdienst bereitstellen

Mit diesem Befehl können Sie den Wetterdienst bereitstellen.

gcloud run deploy $WEATHER_SERVICE \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Wetterdienst testen

Sie können das Wetter an den beiden Standorten mit curl überprüfen:

WEATHER_SERVICE_URL=$(gcloud run services describe $WEATHER_SERVICE \
              --platform managed \
              --region=$REGION \
              --format='value(status.url)')

curl $WEATHER_SERVICE_URL/getweather?location=Seattle

curl $WEATHER_SERVICE_URL/getweather?location\=New%20Orleans

In Seattle liegt die Temperatur bei 40 °C und es ist regnerisch, in New Orleans sind es 37 °C und es ist immer feucht.

6. Front-End-Dienst erstellen

Fügen Sie zuerst „cd“ in das Front-End-Verzeichnis ein.

cd gemini-function-calling/frontend

Erstellen Sie dann eine package.json-Datei mit folgendem Inhalt:

{
  "name": "demo1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node app.js",
    "nodemon": "nodemon app.js",
    "cssdev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
    "tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
    "dev": "npm run tailwind && npm run nodemon"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@google-cloud/vertexai": "^0.4.0",
    "axios": "^1.6.7",
    "express": "^4.18.2",
    "express-ws": "^5.0.2",
    "htmx.org": "^1.9.10"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "tailwindcss": "^3.4.1"
  }
}

Erstellen Sie als Nächstes eine app.js-Quelldatei mit folgendem Inhalt. Diese Datei enthält den Einstiegspunkt für den Dienst und die Hauptlogik für die Anwendung.

const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const path = require("path");

const fs = require("fs");
const util = require("util");
const { spinnerSvg } = require("./spinnerSvg.js");

const expressWs = require("express-ws")(app);

app.use(express.static("public"));

const {
    VertexAI,
    FunctionDeclarationSchemaType
} = require("@google-cloud/vertexai");

// get project and location from metadata service
const metadataService = require("./metadataService.js");

// instance of Gemini model
let generativeModel;

// 1: define the function
const functionDeclarations = [
    {
        function_declarations: [
            {
                name: "getweather",
                description: "get weather for a given location",
                parameters: {
                    type: FunctionDeclarationSchemaType.OBJECT,
                    properties: {
                        location: {
                            type: FunctionDeclarationSchemaType.STRING
                        },
                        degrees: {
                            type: FunctionDeclarationSchemaType.NUMBER,
                            "description":
                                "current temperature in fahrenheit"
                        },
                        conditions: {
                            type: FunctionDeclarationSchemaType.STRING,
                            "description":
                                "how the weather feels subjectively"
                        }
                    },
                    required: ["location"]
                }
            }
        ]
    }
];

// on instance startup
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
    console.log(`demo1: listening on port ${port}`);

    const project = await metadataService.getProjectId();
    const location = await metadataService.getRegion();

    // Vertex client library instance
    const vertex_ai = new VertexAI({
        project: project,
        location: location
    });

    // Instantiate models
    generativeModel = vertex_ai.getGenerativeModel({
        model: "gemini-1.0-pro-001"
    });
});

const axios = require("axios");
const baseUrl = "https://weatherservice-k6msmyp47q-uc.a.run.app";

app.ws("/sendMessage", async function (ws, req) {

    // this chat history will be pinned to the current 
    // Cloud Run instance. Consider using Firestore &
    // Firebase anonymous auth instead.

    // start ephemeral chat session with Gemini
    const chatWithModel = generativeModel.startChat({
        tools: functionDeclarations
    });

    ws.on("message", async function (message) {
        let questionToAsk = JSON.parse(message).message;
        console.log("WebSocket message: " + questionToAsk);

        ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
                        id="questionToAsk"
                        class="text-black m-2 text-right border p-2 rounded-lg ml-24">
                        ${questionToAsk}
                    </div></div>`);

        // to simulate a natural pause in conversation
        await sleep(500);

        // get timestamp for div to replace
        const now = "fromGemini" + Date.now();

        ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
                        id=${now}
                        class=" text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
                        ${spinnerSvg}
                    </div></div>`);

        const results = await chatWithModel.sendMessage(questionToAsk);

        // Function calling demo
        let response1 = await results.response;
        let data = response1.candidates[0].content.parts[0];

        let methodToCall = data.functionCall;
        if (methodToCall === undefined) {
            console.log("Gemini says: ", data.text);
            ws.send(`<div
                        id=${now}
                        hx-swap-oob="true"
                        hx-swap="outerHTML"
                        class="text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
                        ${data.text}
                    </div>`);

            // bail out - Gemini doesn't want to return a function
            return;
        }

        // otherwise Gemini wants to call a function
        console.log(
            "Gemini wants to call: " +
                methodToCall.name +
                " with args: " +
                util.inspect(methodToCall.args, {
                    showHidden: false,
                    depth: null,
                    colors: true
                })
        );

        // make the external call
        let jsonReturned;
        try {
            const responseFunctionCalling = await axios.get(
                baseUrl + "/" + methodToCall.name,

                {
                    params: {
                        location: methodToCall.args.location
                    }
                }
            );
            jsonReturned = responseFunctionCalling.data;
        } catch (ex) {
            // in case an invalid location was provided
            jsonReturned = ex.response.data;
        }

        console.log("jsonReturned: ", jsonReturned);

        // tell the model what function we just called
        const functionResponseParts = [
            {
                functionResponse: {
                    name: methodToCall.name,
                    response: {
                        name: methodToCall.name,
                        content: { jsonReturned }
                    }
                }
            }
        ];

        // // Send a follow up message with a FunctionResponse
        const result2 = await chatWithModel.sendMessage(
            functionResponseParts
        );

        // This should include a text response from the model using the response content
        // provided above
        const response2 = await result2.response;
        let answer = response2.candidates[0].content.parts[0].text;
        console.log("answer: ", answer);

        ws.send(`<div
                        id=${now}
                        hx-swap-oob="true"
                        hx-swap="outerHTML"
                        class="text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
                        ${answer}
                    </div>`);
    });

    ws.on("close", () => {
        console.log("WebSocket was closed");
    });
});

function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

Erstellen Sie eine input.css-Datei für tailwindCSS.

@tailwind base;
@tailwind components;
@tailwind utilities;

Erstellen Sie die Datei tailwind.config.js für tailwindCSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./**/*.{html,js}"],
    theme: {
        extend: {}
    },
    plugins: []
};

Erstellen Sie die Datei metadataService.js, um die Projekt-ID und Region für den bereitgestellten Cloud Run-Dienst abzurufen. Diese Werte werden verwendet, um eine Instanz der Vertex AI-Clientbibliotheken zu instanziieren.

const your_project_id = "YOUR_PROJECT_ID";
const your_region = "YOUR_REGION";

const axios = require("axios");

module.exports = {
    getProjectId: async () => {
        let project = "";
        try {
            // Fetch the token to make a GCF to GCF call
            const response = await axios.get(
                "http://metadata.google.internal/computeMetadata/v1/project/project-id",
                {
                    headers: {
                        "Metadata-Flavor": "Google"
                    }
                }
            );

            if (response.data == "") {
                // running locally on Cloud Shell
                project = your_project_id;
            } else {
                // use project id from metadata service
                project = response.data;
            }
        } catch (ex) {
            // running locally on local terminal
            project = your_project_id;
        }

        return project;
    },

    getRegion: async () => {
        let region = "";
        try {
            // Fetch the token to make a GCF to GCF call
            const response = await axios.get(
                "http://metadata.google.internal/computeMetadata/v1/instance/region",
                {
                    headers: {
                        "Metadata-Flavor": "Google"
                    }
                }
            );

            if (response.data == "") {
                // running locally on Cloud Shell
                region = your_region;
            } else {
                // use region from metadata service
                let regionFull = response.data;
                const index = regionFull.lastIndexOf("/");
                region = regionFull.substring(index + 1);
            }
        } catch (ex) {
            // running locally on local terminal
            region = your_region;
        }
        return region;
    }
};

Datei mit dem Namen „spinnerSvg.js“ erstellen

module.exports.spinnerSvg = `<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500"
                    xmlns="http://www.w3.org/2000/svg"
                    fill="none"
                    viewBox="0 0 24 24"
                >
                    <circle
                        class="opacity-25"
                        cx="12"
                        cy="12"
                        r="10"
                        stroke="currentColor"
                        stroke-width="4"
                    ></circle>
                    <path
                        class="opacity-75"
                        fill="currentColor"
                        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                    ></path></svg>`;

Erstellen Sie ein neues public-Verzeichnis.

mkdir public
cd public

Erstellen Sie nun die Datei index.html für das Front-End, die htmx verwendet.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0"
        />
        <script
            src="https://unpkg.com/htmx.org@1.9.10"
            integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
            crossorigin="anonymous"
        ></script>

        <link href="./output.css" rel="stylesheet" />
        <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>

        <title>Demo 2</title>
    </head>
    <body>
        <div id="herewego" text-center>
            <!-- <div id="replaceme2" hx-swap-oob="true">Hello world</div> -->
            <div
                class="container mx-auto mt-8 text-center max-w-screen-lg"
            >
                <div
                    class="overflow-y-scroll bg-white p-2 border h-[500px] space-y-4 rounded-lg m-auto"
                >
                    <div id="toupdate"></div>
                </div>
                <form
                    hx-trigger="submit, keyup[keyCode==13] from:body"
                    hx-ext="ws"
                    ws-connect="/sendMessage"
                    ws-send=""
                    hx-on="htmx:wsAfterSend: document.getElementById('message').value = ''"
                >
                    <div class="mb-6 mt-6 flex gap-4">
                        <textarea
                            rows="2"
                            type="text"
                            id="message"
                            name="message"
                            class="block grow rounded-lg border p-6 resize-none"
                            required
                        >
What&apos;s is the current weather in Seattle?</textarea
                        >
                        <button
                            type="submit"
                            class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
                        >
                            Send
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </body>
</html>

7. Front-End-Dienst lokal ausführen

Prüfen Sie zuerst, ob Sie sich im Verzeichnis frontend für Ihr Codelab befinden.

cd .. && pwd

Installieren Sie anschließend die Abhängigkeiten, indem Sie den folgenden Befehl ausführen:

npm install

ADC bei lokaler Ausführung verwenden

Wenn die Ausführung in Cloud Shell erfolgt, ist bereits eine virtuelle Maschine der Google Compute Engine vorhanden. Ihre Anmeldedaten, die dieser virtuellen Maschine zugeordnet sind (wie beim Ausführen von gcloud auth list gezeigt), werden automatisch von Standardanmeldedaten für Anwendungen verwendet, daher ist die Verwendung des Befehls gcloud auth application-default login nicht erforderlich. Sie können direkt zum Abschnitt Anwendung lokal ausführen wechseln.

Wenn Sie die Anwendung jedoch auf Ihrem lokalen Terminal ausführen (d.h. nicht in Cloud Shell), müssen Sie die Standardanmeldedaten für Anwendungen zur Authentifizierung bei Google APIs verwenden. Sie können sich entweder 1) mit Ihren Anmeldedaten anmelden (sofern Sie sowohl die Vertex AI-Nutzer- als auch die Datastore-Nutzerrolle haben) oder 2) sich als das in diesem Codelab verwendete Dienstkonto anmelden.

Option 1) Anmeldedaten für ADC verwenden

Wenn Sie Ihre Anmeldedaten verwenden möchten, können Sie zuerst gcloud auth list ausführen, um zu prüfen, wie Sie in gcloud authentifiziert sind. Als Nächstes müssen Sie Ihrer Identität möglicherweise die Rolle „Vertex AI User“ gewähren. Wenn Ihre Identität die Rolle „Inhaber“ hat, haben Sie diese Vertex AI-Nutzerrolle bereits. Falls nicht, können Sie diesen Befehl ausführen, um die Vertex AI-Nutzerrolle und die Datastore-Nutzerrolle zu gewähren.

USER=<YOUR_PRINCIPAL_EMAIL>

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/aiplatform.user

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/datastore.user

Führen Sie dann den folgenden Befehl aus:

gcloud auth application-default login

Option 2) Identität eines Dienstkontos für ADC übernehmen

Wenn Sie das in diesem Codelab erstellte Dienstkonto verwenden möchten, muss Ihr Nutzerkonto die Rolle Ersteller von Dienstkonto-Tokens haben. Sie können diese Rolle mit dem folgenden Befehl abrufen:

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/iam.serviceAccountTokenCreator

Führen Sie als Nächstes den folgenden Befehl aus, um ADC mit dem Dienstkonto zu verwenden.

gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS

Anwendung lokal ausführen

Schließlich können Sie die Anwendung starten, indem Sie das folgende Skript ausführen. Das Entwicklerskript generiert außerdem die Datei „output.css“ aus tailwindCSS.

npm run dev

Sie können eine Vorschau der Website anzeigen, indem Sie die Schaltfläche „Webvorschau“ öffnen und „Vorschauport 8080“ auswählen

Webvorschau – Vorschau auf Port 8080

8. Front-End-Dienst bereitstellen und testen

Führen Sie zuerst diesen Befehl aus, um die Bereitstellung zu starten, und geben Sie das zu verwendende Dienstkonto an. Wenn kein Dienstkonto angegeben ist, wird das Compute-Standarddienstkonto verwendet.

gcloud run deploy $FRONTEND \
  --service-account $SERVICE_ACCOUNT_ADDRESS \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Öffnen Sie die Dienst-URL für das Front-End in Ihrem Browser. Stellen Sie die Frage: „Wie ist das aktuelle Wetter in München?“ und Gemini sollte antworten: „Es ist momentan 40 Grad und regnerisch.“ Wenn Sie fragen: „Wie ist das aktuelle Wetter in Boston?“ Gemini antwortet mit „Ich kann diese Anfrage nicht ausführen. Die verfügbare Wetter-API hat keine Daten für Boston."

9. Glückwunsch!

Herzlichen Glückwunsch zum Abschluss des Codelabs!

Weitere Informationen finden Sie in der Dokumentation zu Cloud Run, Gemini APIs in Vertex AI und Funktionsaufrufen.

Behandelte Themen

  • So funktionieren Gemini-Funktionsaufrufe
  • Eine Gemini-basierte Chatbot-App als Cloud Run-Dienst bereitstellen

10. Bereinigen

Um versehentliche Gebühren zu vermeiden, z. B. wenn diese Cloud Run-Dienstleistung versehentlich mehr Mal aufgerufen wird als Ihre monatliche Zuweisung von Cloud Run-Aufrufen in der kostenlosen Stufe, können Sie entweder den Cloud Run-Dienst oder das in Schritt 2 erstellte Projekt löschen.

Wenn Sie die Cloud Run-Dienste löschen möchten, rufen Sie die Cloud Run-Cloud Console unter https://console.cloud.google.com/functions/ auf und löschen Sie die $PROPERTY_SERVICE- und $FRONTEND-Dienste, die Sie in diesem Codelab erstellt haben.

Sie sollten auch das Dienstkonto „vertex-ai-caller“ löschen oder die Rolle „Vertex AI User“ widerrufen, um unbeabsichtigte Aufrufe an Gemini zu vermeiden.

Wenn Sie das gesamte Projekt löschen möchten, rufen Sie https://console.cloud.google.com/cloud-resource-manager auf, wählen Sie das in Schritt 2 erstellte Projekt aus und klicken Sie auf „Löschen“. Wenn Sie das Projekt löschen, müssen Sie die Projekte in Ihrem Cloud SDK ändern. Sie können die Liste aller verfügbaren Projekte mit dem Befehl gcloud projects list aufrufen.