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

1. Wprowadzenie

Przegląd

W tym ćwiczeniu dowiesz się, jak przyznać Gemini dostęp do danych w czasie rzeczywistym za pomocą nowej funkcji o nazwie Wywoływanie funkcji. Aby symulować dane w czasie rzeczywistym, utworzysz punkt końcowy usługi pogodowej, który zwraca aktualną pogodę w 2 lokalizacjach. Następnie utworzysz komunikator oparty na Gemini, który korzysta z wywoływania funkcji do pobierania aktualnej pogody.

Aby lepiej zrozumieć wywoływanie funkcji, przyjrzyjmy się szybkiemu przykładowi.

  • Prompt zawiera prośbę o podanie aktualnej pogody w danej lokalizacji.
  • Ten prompt i kontrakt funkcji getWeather() są wysyłane do Gemini.
  • Gemini prosi aplikację chatbota o wywołanie w jego imieniu funkcji „getWeather(Seattle)”
  • Aplikacja odsyła wyniki (40 stopni Fahrenheita i deszcz).
  • Gemini odsyła wyniki do elementu wywołującego

Podsumowując, Gemini nie wywołuje funkcji. Deweloper musi wywołać funkcję i odesłać wyniki do Gemini.

Diagram przepływu wywoływania funkcji

Czego się nauczysz

  • Jak działa wywoływanie funkcji w Gemini
  • Jak wdrożyć aplikację do obsługi czatu 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 tego środowiska. Jeśli pojawił się ekran pośredni, kliknij Dalej.

d95252b003979716.png

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

7833d5e1c5d18f54.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Większość zadań w tym ćwiczeniu, a być może wszystkie, możesz wykonać w przeglądarce.

Po połączeniu z Cloud Shell zobaczysz, że uwierzytelnianie zostało już przeprowadzone, a projekt jest już ustawiony na Twój identyfikator projektu.

  1. Aby potwierdzić, że uwierzytelnianie zostało przeprowadzone, uruchom w Cloud Shell to polecenie:
gcloud auth list

Wynik polecenia

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Aby potwierdzić, że polecenie gcloud zna Twój projekt, uruchom w Cloud Shell to polecenie:
gcloud config list project

Wynik polecenia

[core]
project = <PROJECT_ID>

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

gcloud config set project <PROJECT_ID>

Wynik polecenia

Updated property [core/project].

3. Konfigurowanie zmiennych środowiskowych i włączanie interfejsów API

Konfigurowanie zmiennych środowiskowych

Możesz ustawić zmienne środowiskowe, których będziesz używać podczas naszych ćwiczeń 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 w Codelabs, musisz włączyć kilka interfejsów API. W tym ćwiczeniu musisz użyć tych 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. Tworzenie konta usługi do wywoływania 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"

Po drugie, 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 kodu źródłowego i przejdź do niego.

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

Następnie utwórz plik package.json o tej treś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ą treś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!");
});

Wdrażanie usługi pogodowej

Za pomocą tego polecenia możesz wdrożyć usługę pogodową.

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

Testowanie usługi pogodowej

Możesz sprawdzić pogodę w 2 lokalizacjach za pomocą polecenia 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 Fahrenheita i pada deszcz, a w Nowym Orleanie jest 99 stopni Fahrenheita i zawsze jest wilgotno.

6. Tworzenie usługi frontendowej

Najpierw przejdź do katalogu frontend.

cd gemini-function-calling/frontend

Następnie utwórz plik package.json o tej treś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ą treś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 uzyskać identyfikator projektu i region wdrożonej usługi Cloud Run. Te wartości zostaną użyte 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 użytkownika, który będzie korzystać z biblioteki 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 dla swojego ćwiczenia.

cd .. && pwd

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

npm install

Używanie ADC podczas uruchamiania lokalnego

Jeśli korzystasz z Cloud Shell, używasz już maszyny wirtualnej Google Compute Engine. Dane logowania powiązane z tą maszyną wirtualną (widoczne po uruchomieniu polecenia gcloud auth list) będą automatycznie używane przez domyślne uwierzytelnianie aplikacji, więc nie musisz używać polecenia gcloud auth application-default login. Możesz przejść do sekcji Lokalne uruchamianie aplikacji.

Jeśli jednak korzystasz z lokalnego terminala (czyli nie z Cloud Shell), do uwierzytelniania w interfejsach API Google musisz używać domyślnego uwierzytelniania aplikacji. Możesz 1) zalogować się za pomocą swoich danych logowania (jeśli masz role Użytkownik Vertex AI i Użytkownik Datastore) lub 2) zalogować się, przyjmując tożsamość konta usługi użytego w tym ćwiczeniu.

Opcja 1. Używanie danych logowania do domyślnego uwierzytelniania aplikacji

Jeśli chcesz użyć swoich danych logowania, możesz najpierw uruchomić polecenie gcloud auth list, aby sprawdzić, jak uwierzytelniasz się w gcloud. Następnie może być konieczne przyznanie tożsamości roli użytkownika Vertex AI. Jeśli Twoja tożsamość ma rolę Właściciel, masz już tę rolę użytkownika Vertex AI. Jeśli nie, możesz uruchomić to polecenie, aby przyznać swojej tożsamości rolę użytkownika Vertex AI 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. Przyjmowanie tożsamości konta usługi na potrzeby ADC

Jeśli chcesz użyć konta usługi utworzonego w tym ćwiczeniu, Twoje konto użytkownika musi mieć rolę twórcy tokenów konta usługi. Aby uzyskać tę rolę, uruchom to polecenie:

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

Następnie uruchom to 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ę, wykonując ten skrypt. Ten skrypt deweloperski wygeneruje też plik output.css z biblioteki tailwindCSS.

npm run dev

Aby wyświetlić podgląd witryny, otwórz przycisk Podgląd w przeglądarce i wybierz Podgląd na porcie 8080.

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

8. Wdrażanie i testowanie usługi frontendu

Najpierw uruchom to polecenie, aby rozpocząć wdrażanie i określić konto usługi, które ma być używane. Jeśli nie podasz konta usługi, użyte zostanie 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 frontendu. Zadaj pytanie „Jaka jest teraz pogoda w Warszawie?” i sprawdź, czy Gemini odpowie „Jest teraz 5°C i pada deszcz”. Jeśli zapytasz „Jaka jest teraz pogoda w Warszawie?”, Gemini odpowie: „Nie mogę zrealizować tej prośby. Dostępny interfejs Weather API nie zawiera danych o Bostonie”.

9. Gratulacje!

Gratulujemy ukończenia ćwiczenia!

Zalecamy zapoznanie się z dokumentacją Cloud Run, Vertex AI Gemini APIwywoływania funkcji.

Omówione zagadnienia

  • Jak działa wywoływanie funkcji w Gemini
  • Jak wdrożyć aplikację do obsługi czatu opartą na Gemini jako usługę Cloud Run

10. Czyszczenie danych

Aby uniknąć przypadkowych opłat (np. jeśli ta usługa Cloud Run zostanie przypadkowo wywołana więcej razy niż miesięczny limit wywołań Cloud Run w warstwie bezpłatnej), możesz usunąć usługę Cloud Run lub projekt utworzony w kroku 2.

Aby usunąć usługi Cloud Run, otwórz konsolę Cloud Run w Google Cloud pod adresem https://console.cloud.google.com/functions/ i usuń usługi $WEATHER_SERVICE i $FRONTEND utworzone w tym ćwiczeniu.

Możesz też usunąć vertex-ai-caller konto usługi lub cofnąć rolę użytkownika Vertex AI, aby uniknąć przypadkowych wywołań Gemini.

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