Jak używać Cloud Run z wywołaniami funkcji Gemini

1. Wprowadzenie

Omówienie

Dzięki temu ćwiczeniu w Codelabs dowiesz się, jak za pomocą nowej funkcji wywoływania funkcji zapewnić Gemini dostęp do danych w czasie rzeczywistym. Aby symulować dane w czasie rzeczywistym, utworzysz punkt końcowy usługi pogody, który zwróci aktualną pogodę w 2 lokalizacjach. Następnie utworzysz aplikację do obsługi czatu korzystającą z technologii Gemini, która będzie korzystać z wywołań funkcji, aby pobierać aktualną prognozę pogody.

Aby zapoznać się z wywołaniami funkcji, spójrzmy na krótką grafikę.

  • Prośba o podanie aktualnej pogody w określonej lokalizacji
  • Ten prompt i kontrakt funkcji getWeather() są wysyłane do Gemini
  • Gemini pyta, czy aplikacja czatbota nazywa się „getWeather(Seattle)”. w jej imieniu
  • Aplikacja zwróci wyniki (40 stopni F i deszcz)
  • Gemini odsyła wyniki do rozmówcy

Podsumowując, Gemini nie wywołuje funkcji. Jako deweloper musisz wywołać tę funkcję i przesłać wyniki do Gemini.

Schemat procesu wywoływania funkcji

Czego się nauczysz

  • Jak działa funkcja Gemini
  • Jak wdrożyć aplikację czatbota opartą na Gemini jako usługę Cloud Run

2. Konfiguracja i wymagania

Wymagania wstępne

Aktywowanie Cloud Shell

  1. W konsoli Cloud kliknij Aktywuj Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tej usługi. Jeśli wyświetlił się ekran pośredni, kliknij Dalej.

d95252b003979716.png

Uzyskanie dostępu do Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.

7833d5e1c5d18f54.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i uwierzytelnianie. Większość zadań w ramach tego ćwiczenia z programowania można wykonać w przeglądarce.

Po nawiązaniu połączenia z Cloud Shell powinno pojawić się potwierdzenie, że użytkownik jest uwierzytelniony, a projekt jest ustawiony na identyfikator Twojego projektu.

  1. Uruchom to polecenie w Cloud Shell, aby potwierdzić, że jesteś uwierzytelniony:
gcloud auth list

Dane wyjściowe polecenia

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Uruchom to polecenie w Cloud Shell, aby sprawdzić, czy polecenie gcloud zna Twój projekt:
gcloud config list project

Dane wyjściowe polecenia

[core]
project = <PROJECT_ID>

Jeśli tak nie jest, możesz go ustawić za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Dane wyjściowe polecenia

Updated property [core/project].

3. Skonfiguruj zmienne środowiskowe i włącz interfejsy API

Skonfiguruj zmienne środowiskowe

Możesz ustawić zmienne środowiskowe, które będą używane podczas tego ćwiczenia z programowania.

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

Włącz interfejsy API

Zanim zaczniesz korzystać z tego ćwiczenia z programowania, musisz włączyć kilka interfejsów API. To ćwiczenie w Codelabs wymaga używania poniższych interfejsów API. Możesz włączyć te interfejsy API, uruchamiając to polecenie:

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

4. Utwórz konto usługi, aby wywołać Vertex AI

To konto usługi będzie używane przez Cloud Run do wywoływania interfejsu Vertex AI Gemini API.

Najpierw utwórz konto usługi, uruchamiając to polecenie:

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

Następnie przypisz do konta usługi rolę użytkownika Vertex AI.

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

5. Tworzenie usługi backendu Cloud Run

Najpierw utwórz katalog na kod źródłowy i cd w tym katalogu.

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

Następnie utwórz plik package.json z tą zawartością:

{
    "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"
    }
}

Następnie utwórz plik źródłowy app.js z poniższą zawartością. Ten plik zawiera punkt wejścia usługi i główną logikę aplikacji.

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!");
});

Wdróż usługę pogody

Aby wdrożyć usługę pogody, możesz użyć tego polecenia.

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

Przetestuj usługę pogodową

Możesz sprawdzić pogodę dla 2 lokalizacji, korzystając z curl:

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

W Seattle jest 40 stopni Celsjusza, a w Nowym Orleanie pada deszcz, a w Nowym Orleanie jest 99 stopni F i zawsze wilgotno.

6. Tworzenie usługi frontendu

Najpierw przejdź do katalogu frontendu (cd).

cd gemini-function-calling/frontend

Następnie utwórz plik package.json z tą zawartością:

{
  "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"
  }
}

Następnie utwórz plik źródłowy app.js z poniższą zawartością. Ten plik zawiera punkt wejścia usługi i główną logikę aplikacji.

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);
    });
}

Utwórz plik input.css dla tailwindCSS.

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

Utwórz plik tailwind.config.js dla tailwindCSS.

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

Utwórz plik metadataService.js, aby pobrać identyfikator projektu i region wdrożonej usługi Cloud Run. Wartości te zostaną wykorzystane do utworzenia instancji bibliotek klienta Vertex AI.

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;
    }
};

Utwórz plik o nazwie spinnerSvg.js.

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>`;

Utwórz nowy katalog public.

mkdir public
cd public

Teraz utwórz plik index.html dla interfejsu, który będzie zawierał tekst htmx.

<!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. Lokalne uruchamianie usługi frontendu

Najpierw sprawdź, czy jesteś w katalogu frontend do ćwiczenia w Codelabs.

cd .. && pwd

Następnie zainstaluj zależności, uruchamiając to polecenie:

npm install

Korzystanie z ADC w przypadku uruchomienia lokalnego

Jeżeli korzystasz z Cloud Shell, oznacza to, że korzystasz już z maszyny wirtualnej Google Compute Engine. Twoje dane logowania powiązane z tą maszyną wirtualną (wyświetlane przez uruchomienie gcloud auth list) będą automatycznie używane przez domyślne dane uwierzytelniające aplikacji, dlatego nie musisz używać polecenia gcloud auth application-default login. Możesz przejść do sekcji Uruchamianie aplikacji lokalnie.

Jeśli jednak korzystasz z terminala lokalnego (tzn. nie w Cloud Shell), do uwierzytelniania w interfejsach API Google musisz używać domyślnych danych logowania aplikacji. Możesz 1) zalogować się za pomocą swoich danych logowania (pod warunkiem, że masz zarówno role użytkownika Vertex AI, jak i Datastore) lub 2) zalogować się, używając konta usługi użytego w tym ćwiczeniu z programowania.

Opcja 1. Używanie Twoich danych logowania do ADC

Jeśli chcesz użyć danych logowania, najpierw uruchom polecenie gcloud auth list, aby sprawdzić sposób uwierzytelniania w gcloud. Następnie może być konieczne przypisanie do tożsamości roli użytkownika Vertex AI. Jeśli Twoja tożsamość ma rolę właściciela, masz już tę rolę użytkownika Vertex AI. Jeśli nie, możesz uruchomić to polecenie, aby przypisać rolę użytkownika Vertex AI tożsamości i rolę użytkownika Datastore.

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

Następnie uruchom to polecenie

gcloud auth application-default login

Opcja 2. Odgrywanie roli konta usługi na potrzeby ADC

Jeśli chcesz używać konta usługi utworzonego w ramach tego ćwiczenia w Codelabs, Twoje konto użytkownika musi mieć przypisaną rolę twórcy tokenów konta usługi. Aby uzyskać tę rolę, uruchom następujące polecenie:

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

Następnie uruchom poniższe polecenie, aby użyć ADC z kontem usługi

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

Lokalne uruchamianie aplikacji

Na koniec możesz uruchomić aplikację, uruchamiając poniższy skrypt. Ten skrypt deweloperski wygeneruje też plikoutput.css z tailwindCSS.

npm run dev

Aby wyświetlić podgląd strony, otwórz przycisk Podgląd w przeglądarce i wybierz Port podglądu 8080

podgląd w przeglądarce – przycisk podglądu na porcie 8080

8. Wdrażanie i testowanie usługi frontendu

Najpierw uruchom to polecenie, aby rozpocząć wdrożenie i określić konto usługi, które ma być używane. Jeśli konto usługi nie jest określone, zostanie użyte domyślne konto usługi Compute.

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

Otwórz w przeglądarce adres URL usługi dla frontendu. Zadaj pytanie „Jaka jest aktualna pogoda w Warszawie?” a Gemini powinna odpowiedzieć „Obecnie jest 40 stopni i pada deszcz”. Jeśli zapytasz „Jaka jest aktualna pogoda w Gdańsku?”, Gemini odpowie „Nie mogę spełnić tej prośby. Dostępny interfejs API pogodowy nie ma danych dla Bostonu”.

9. Gratulacje!

Gratulujemy ukończenia ćwiczeń z programowania.

Zalecamy zapoznanie się z dokumentacją Cloud Run, interfejsów Vertex AI Gemini API oraz wywołaniami funkcji.

Omówione zagadnienia

  • Jak działa funkcja Gemini
  • Jak wdrożyć aplikację czatbota opartą na Gemini jako usługę Cloud Run

10. Czyszczenie danych

Aby uniknąć niezamierzonych opłat (na przykład jeśli ta usługa Cloud Run została przypadkowo wywołana więcej razy niż miesięczna alokacja wywołań Cloud Run na poziomie bezpłatnym), możesz usunąć usługę Cloud Run lub projekt utworzony w kroku 2.

Aby usunąć usługi Cloud Run, otwórz konsolę Cloud Run na stronie https://console.cloud.google.com/functions/ i usuń usługi $WEATHER_SERVICE oraz $FRONTEND utworzone podczas tego ćwiczenia z programowania.

Aby uniknąć niezamierzonych wywołań Gemini, możesz też usunąć konto usługi vertex-ai-caller lub anulować rolę użytkownika Vertex AI.

Jeśli zdecydujesz się usunąć cały projekt, możesz otworzyć stronę https://console.cloud.google.com/cloud-resource-manager, wybrać projekt utworzony w kroku 2 i kliknąć Usuń. Jeśli usuniesz projekt, musisz zmienić projekty w pakiecie SDK Cloud. Aby wyświetlić listę wszystkich dostępnych projektów, uruchom polecenie gcloud projects list.