Gemini İşlevi Çağrısı ile Cloud Run'ı Kullanma

1. Giriş

Genel Bakış

Bu codelab'de, işlev çağrısı adlı yeni bir özelliği kullanarak Gemini'a gerçek zamanlı verilere erişim izni verme adımları gösterilmektedir. Anlık verileri simüle etmek için 2 konumun mevcut hava durumunu döndüren bir hava durumu hizmeti uç noktası oluşturacaksınız. Ardından, mevcut hava durumunu almak için işlev çağrısı kullanan, Gemini destekli bir sohbet uygulaması oluşturacaksınız.

İşlev çağrısını anlamak için hızlı bir görsel kullanalım.

  • İstem, belirli bir konumdaki mevcut hava durumu konumlarını istiyor
  • Bu istem ve getWeather() işlev sözleşmesi Gemini'a gönderilir.
  • Gemini, sohbet botu uygulamasının kendi adına "getWeather(Seattle)" işlevini çağırmasını istiyor.
  • Uygulama sonuçları geri gönderir (4,4 °C ve yağmurlu).
  • Gemini, sonuçları arayana geri gönderir.

Özetlemek gerekirse Gemini, işlevi çağırmaz. Geliştirici olarak işlevi çağırmalı ve sonuçları Gemini'a geri göndermelisiniz.

İşlev çağrısı akışının şeması

Neler öğreneceksiniz?

  • Gemini işlev çağrısının işleyiş şekli
  • Gemini destekli bir chatbot uygulamasını Cloud Run hizmeti olarak dağıtma

2. Kurulum ve Gereksinimler

Ön koşullar

Cloud Shell'i etkinleştirme

  1. Cloud Console'da Cloud Shell'i etkinleştir 'i d1264ca30785e435.png tıklayın.

cb81e7c8e34bc8d.png

Cloud Shell'i ilk kez başlatıyorsanız ne olduğunu açıklayan bir ara ekran gösterilir. Ara ekran gösterildiyse Devam'ı tıklayın.

d95252b003979716.png

Cloud Shell'in temel hazırlığı ve bağlanması yalnızca birkaç dakikanızı alır.

7833d5e1c5d18f54.png

Bu sanal makineye, ihtiyaç duyacağınız tüm geliştirme araçları yüklenmiştir. 5 GB boyutunda kalıcı bir ana dizin bulunur ve Google Cloud'da çalışır. Bu sayede ağ performansı ve kimlik doğrulama önemli ölçüde güçlenir. Bu codelab'deki çalışmalarınızın neredeyse tamamını tarayıcıyla yapabilirsiniz.

Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin, proje kimliğinize ayarlandığını görürsünüz.

  1. Kimliğinizin doğrulandığını onaylamak için Cloud Shell'de şu komutu çalıştırın:
gcloud auth list

Komut çıkışı

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. gcloud komutunun projeniz hakkında bilgi sahibi olduğunu onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud config list project

Komut çıkışı

[core]
project = <PROJECT_ID>

Değilse şu komutla ayarlayabilirsiniz:

gcloud config set project <PROJECT_ID>

Komut çıkışı

Updated property [core/project].

3. Ortam değişkenlerini ayarlama ve API'leri etkinleştirme

Ortam değişkenlerini ayarlama

Bu codelab boyunca kullanılacak ortam değişkenlerini ayarlayabilirsiniz.

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

API'leri etkinleştir

Bu codelab'i kullanmaya başlamadan önce etkinleştirmeniz gereken birkaç API vardır. Bu codelab'de aşağıdaki API'lerin kullanılması gerekir. Bu API'leri aşağıdaki komutu çalıştırarak etkinleştirebilirsiniz:

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

4. Vertex AI'ı çağırmak için hizmet hesabı oluşturma

Bu hizmet hesabı, Vertex AI Gemini API'yi çağırmak için Cloud Run tarafından kullanılır.

Öncelikle şu komutu çalıştırarak hizmet hesabını oluşturun:

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

İkinci olarak, hizmet hesabına Vertex AI Kullanıcısı rolünü verin.

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

5. Arka uç Cloud Run hizmetini oluşturma

Öncelikle kaynak kodu için bir dizin oluşturun ve bu dizine gidin.

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

Ardından, aşağıdaki içeriğe sahip bir package.json dosyası oluşturun:

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

Ardından, aşağıdaki içeriğe sahip bir app.js kaynak dosyası oluşturun. Bu dosya, hizmetin giriş noktasını ve uygulamanın ana mantığını içerir.

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

Hava durumu hizmetini dağıtma

Hava durumu hizmetini dağıtmak için bu komutu kullanabilirsiniz.

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

Hava durumu hizmetini test etme

2 konumun hava durumunu curl kullanarak doğrulayabilirsiniz:

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

Seattle'da sıcaklık 40 derece ve hava yağmurlu, New Orleans'ta ise sıcaklık 99 derece ve hava her zaman nemli.

6. Ön uç hizmetini oluşturun

Öncelikle, cd komutunu kullanarak ön uç dizinine gidin.

cd gemini-function-calling/frontend

Ardından, aşağıdaki içeriğe sahip bir package.json dosyası oluşturun:

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

Ardından, aşağıdaki içeriğe sahip bir app.js kaynak dosyası oluşturun. Bu dosya, hizmetin giriş noktasını ve uygulamanın ana mantığını içerir.

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

TailwindCSS için bir input.css dosyası oluşturun.

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

tailwindCSS için tailwind.config.js dosyasını oluşturun.

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

Dağıtılan Cloud Run hizmetinin proje kimliğini ve bölgesini almak için metadataService.js dosyasını oluşturun. Bu değerler, Vertex AI istemci kitaplıklarının bir örneğini oluşturmak için kullanılır.

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

spinnerSvg.js adlı bir dosya oluşturun.

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

Yeni bir public dizini oluşturun.

mkdir public
cd public

Şimdi htmx kullanacak olan index.html dosyasını kullanıcı arabirimi için oluşturun.

<!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. Ön uç hizmetini yerel olarak çalıştırma

Öncelikle, codelab'iniz için frontend dizininde olduğunuzdan emin olun.

cd .. && pwd

Ardından, aşağıdaki komutu çalıştırarak bağımlılıkları yükleyin:

npm install

Yerel olarak çalıştırırken ADC'yi kullanma

Cloud Shell'de çalışıyorsanız zaten bir Google Compute Engine sanal makinesinde çalışıyorsunuzdur. Bu sanal makineyle ilişkili kimlik bilgileriniz (gcloud auth list çalıştırılarak gösterildiği gibi) Uygulama Varsayılan Kimlik Bilgileri tarafından otomatik olarak kullanılacağından gcloud auth application-default login komutunu kullanmanız gerekmez. Uygulamayı yerel olarak çalıştırma bölümüne atlayabilirsiniz.

Ancak yerel terminalinizde (ör. Cloud Shell'de değil) çalışıyorsanız Google API'lerinde kimlik doğrulaması yapmak için Uygulama Varsayılan Kimlik Bilgileri'ni kullanmanız gerekir. 1) Kimlik bilgilerinizi kullanarak giriş yapabilir (hem Vertex AI Kullanıcısı hem de Veri Deposu Kullanıcısı rollerine sahip olmanız gerekir) veya 2) bu codelab'de kullanılan hizmet hesabının kimliğine bürünerek giriş yapabilirsiniz.

1. seçenek: ADC için kimlik bilgilerinizi kullanma

Kimlik bilgilerinizi kullanmak istiyorsanız öncelikle gcloud auth list komutunu çalıştırarak gcloud'da nasıl kimlik doğrulandığınızı doğrulayabilirsiniz. Ardından, kimliğinize Vertex AI Kullanıcısı rolünü vermeniz gerekebilir. Kimliğinizde Sahip rolü varsa bu Vertex AI kullanıcı rolüne zaten sahipsinizdir. Aksi takdirde, kimliğinize Vertex AI kullanıcı rolü ve veri deposu kullanıcı rolü vermek için bu komutu çalıştırabilirsiniz.

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

Ardından aşağıdaki komutu çalıştırın.

gcloud auth application-default login

2. seçenek: ADC için bir hizmet hesabının kimliğine bürünme

Bu codelab'de oluşturulan hizmet hesabını kullanmak istiyorsanız kullanıcı hesabınızın Hizmet Hesabı Jetonu Oluşturucu rolüne sahip olması gerekir. Bu rolü aşağıdaki komutu çalıştırarak edinebilirsiniz:

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

Ardından, hizmet hesabıyla ADC'yi kullanmak için aşağıdaki komutu çalıştıracaksınız.

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

Uygulamayı yerel olarak çalıştırma

Son olarak, aşağıdaki komut dosyasını çalıştırarak uygulamayı başlatabilirsiniz. Bu geliştirme komut dosyası, tailwindCSS'den output.css dosyasını da oluşturur.

npm run dev

Web Önizlemesi düğmesini açıp Önizleme Bağlantı Noktası 8080'i seçerek web sitesini önizleyebilirsiniz.

web preview - preview on port 8080 button

8. Ön uç hizmetini dağıtma ve test etme

Öncelikle, dağıtımı başlatmak ve kullanılacak hizmet hesabını belirtmek için bu komutu çalıştırın. Hizmet hesabı belirtilmezse varsayılan Compute hizmet hesabı kullanılır.

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

Tarayıcınızda ön uç için hizmet URL'sini açın. "Seattle'da şu anda hava nasıl?" diye sorun. Gemini, "Şu anda hava 40 derece ve yağmurlu." şeklinde yanıt vermelidir. "Boston'da şu an hava nasıl?" diye sorarsanız Gemini, "Bu isteği yerine getiremiyorum. Kullanılabilir hava durumu API'sinde Boston'a ait veri yok."

9. Tebrikler!

Tebrikler, codelab'i tamamladınız.

Cloud Run, Vertex AI Gemini API'leri ve işlev çağrısı belgelerini incelemenizi öneririz.

İşlediğimiz konular

  • Gemini işlev çağrısının işleyiş şekli
  • Gemini destekli bir chatbot uygulamasını Cloud Run hizmeti olarak dağıtma

10. Temizleme

Yanlışlıkla ücretlendirilmemek için (örneğin, bu Cloud Run hizmeti ücretsiz katmandaki aylık Cloud Run çağırma tahsisinizden daha fazla sayıda yanlışlıkla çağrılırsa) Cloud Run hizmetini veya 2. adımda oluşturduğunuz projeyi silebilirsiniz.

Cloud Run hizmetlerini silmek için https://console.cloud.google.com/functions/ adresinden Cloud Run Cloud Console'a gidin ve bu codelab'de oluşturduğunuz $WEATHER_SERVICE ve $FRONTEND hizmetlerini silin.

Gemini'a yanlışlıkla yapılan çağrıları önlemek için vertex-ai-caller hizmet hesabını silebilir veya Vertex AI Kullanıcısı rolünü iptal edebilirsiniz.

Projenin tamamını silmeyi tercih ederseniz https://console.cloud.google.com/cloud-resource-manager adresine gidebilir, 2. adımda oluşturduğunuz projeyi seçip Sil'i tıklayabilirsiniz. Projeyi silerseniz Cloud SDK'nızda projeleri değiştirmeniz gerekir. gcloud projects list komutunu çalıştırarak kullanılabilir tüm projelerin listesini görüntüleyebilirsiniz.