איך משתמשים ב-Cloud Run עם קריאה לפונקציה של Gemini

1. מבוא

סקירה כללית

ב-Codelab הזה נסביר איך אפשר לתת ל-Gemini גישה לנתונים בזמן אמת באמצעות תכונה חדשה בשם קריאה לפונקציות. כדי לדמות נתונים בזמן אמת, צריך ליצור נקודת קצה (endpoint) של שירות מזג אוויר שמחזירה את מזג האוויר הנוכחי בשני מיקומים. לאחר מכן בונים אפליקציית צ'אט, שמבוססת על Gemini, שמשתמשת בקריאות פונקציות כדי לאחזר את מזג האוויר הנוכחי.

נשתמש בתצוגה חזותית מהירה כדי להבין את התכונה 'קריאות לפונקציות'.

  • הבקשה נשלחת למיקומים שבהם יש מזג אוויר נוכחי במיקום נתון
  • ההנחיה הזו + החוזה של הפונקציה getWeather() נשלחים אל Gemini
  • Gemini מבקש שאפליקציית הצ'אט בוט תקרא 'getWeather(Seattle)' מטעמו
  • האפליקציה שולחת חזרה את התוצאות (40 מעלות F וגשם)
  • Gemini שולח בחזרה את התוצאות למי שהתקשר

לסיכום, Gemini לא קורא לפונקציה. כמפתח, אתם צריכים לקרוא לפונקציה ולשלוח את התוצאות בחזרה ל-Gemini.

תרשים זרימה של הפונקציה

מה תלמדו

  • הסבר על שליחת פונקציות של Gemini
  • איך פורסים אפליקציית צ'אט בוט שמבוססת על Gemini בתור שירות Cloud Run

2. הגדרה ודרישות

דרישות מוקדמות

הפעלת Cloud Shell

  1. במסוף Cloud, לוחצים על Activate Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

אם זו הפעם הראשונה שאתם מפעילים את Cloud Shell, יוצג לכם מסך ביניים שמתוארת בו. אם הוצג לכם מסך ביניים, לוחצים על המשך.

d95252b003979716.png

ההקצאה וההתחברות ל-Cloud Shell נמשכת כמה דקות.

7833d5e1c5d18f54.png

במכונה הווירטואלית הזו נמצאים כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. אם לא את כולן, ניתן לבצע חלק גדול מהעבודה ב-Codelab הזה באמצעות דפדפן.

אחרי ההתחברות ל-Cloud Shell, אתם אמורים לראות שהפרויקט מאומת ושהפרויקט מוגדר לפי מזהה הפרויקט שלכם.

  1. מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שהאימות בוצע:
gcloud auth list

פלט הפקודה

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שהפקודה ב-gcloud יודעת על הפרויקט שלכם:
gcloud config list project

פלט הפקודה

[core]
project = <PROJECT_ID>

אם היא לא נמצאת שם, תוכלו להגדיר אותה באמצעות הפקודה הבאה:

gcloud config set project <PROJECT_ID>

פלט הפקודה

Updated property [core/project].

3. הגדרה של משתני סביבה והפעלת ממשקי API

הגדרה של משתני סביבה

אתם יכולים להגדיר משתני סביבה שישמשו ב-Codelab הזה.

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

לפני שתוכלו להשתמש ב-Codelab הזה, יש כמה ממשקי API שתצטרכו להפעיל. ה-Codelab הזה מחייב שימוש בממשקי ה-API הבאים. כדי להפעיל את ממשקי ה-API האלה, מריצים את הפקודה הבאה:

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

4. צריך ליצור חשבון שירות כדי לקרוא ל-Vertex AI

חשבון השירות הזה ישמש את Cloud Run כדי לקרוא ל-Vertex AI Gemini API.

קודם כול, יוצרים את חשבון השירות באמצעות הפקודה הבאה:

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

לאחר מכן, מקצים לחשבון השירות את התפקיד Vertex AI User.

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

5. יצירת שירות Cloud Run לקצה העורפי

קודם כל, יוצרים ספרייה עבור קוד המקור וה-cd בספרייה הזו.

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

לאחר מכן, יוצרים קובץ package.json עם התוכן הבא:

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

בשלב הבא, יוצרים קובץ מקור app.js עם התוכן שלמטה. הקובץ הזה מכיל את נקודת הכניסה לשירות ומכיל את הלוגיקה העיקרית של האפליקציה.

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

פריסת שירות מזג האוויר

אפשר להשתמש בפקודה הזו כדי לפרוס את שירות מזג האוויר.

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

בדיקת שירות מזג האוויר

אפשר לבדוק את מזג האוויר בשני המיקומים באמצעות 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

הטמפרטורה בסיאטל היא 40 מעלות צלזיוס, והטמפרטורה בניו אורלינס היא 99 מעלות צלזיוס ולחות תמיד.

6. יצירת שירות Frontend

קודם כול, cd לספריית הקצה הקדמי.

cd gemini-function-calling/frontend

לאחר מכן, יוצרים קובץ package.json עם התוכן הבא:

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

בשלב הבא, יוצרים קובץ מקור app.js עם התוכן שלמטה. הקובץ הזה מכיל את נקודת הכניסה לשירות ומכיל את הלוגיקה העיקרית של האפליקציה.

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

יוצרים קובץ input.css בשביל tailwindCSS.

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

יוצרים את הקובץ tailwind.config.js עבור tailwindCSS.

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

יוצרים את הקובץ metadataService.js לקבלת מזהה הפרויקט והאזור של שירות Cloud Run שנפרס. הערכים האלה ישמשו כדי ליצור מכונה של ספריות הלקוח של 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;
    }
};

יצירת קובץ בשם 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>`;

יוצרים ספריית public חדשה.

mkdir public
cd public

עכשיו יוצרים את הקובץ index.html עבור ממשק הקצה, שישתמש ב-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. הפעלת שירות Frontend באופן מקומי

קודם כל, צריך לוודא שאתם נמצאים בספרייה frontend של ה-Codelab.

cd .. && pwd

לאחר מכן מריצים את הפקודה הבאה כדי להתקין את יחסי התלות:

npm install

שימוש ב-ADC כשמריצים באופן מקומי

אם אתם משתמשים ב-Cloud Shell, סימן שאתם כבר משתמשים במכונה וירטואלית של Google Compute Engine. פרטי הכניסה שמשויכים למכונה הווירטואלית הזו (כפי שמוצג בהרצה של gcloud auth list) ישמשו באופן אוטומטי את Application Default Credentials, כך שאין צורך להשתמש בפקודה gcloud auth application-default login. אפשר לדלג למטה לקטע הפעלת האפליקציה באופן מקומי

עם זאת, אם אתם מריצים אותו בטרמינל המקומי (כלומר לא ב-Cloud Shell), תצטרכו להשתמש ב-Application Default Credentials כדי לבצע אימות ל-Google APIs. אפשר 1) להתחבר באמצעות פרטי הכניסה שלכם (בתנאי שיש לכם גם תפקיד משתמש ב-Vertex AI וגם משתמש ב-Datastore) או 2) להתחבר באמצעות התחזות לחשבון השירות שבו השתמשתם ב-Codelab הזה.

אפשרות 1) שימוש בפרטי הכניסה ל-ADC

אם תרצו להשתמש בפרטי הכניסה שלכם, תוכלו קודם להריץ את gcloud auth list כדי לאמת את תהליך האימות ב-gcloud. בשלב הבא, ייתכן שתצטרכו להעניק לזהות שלכם את התפקיד Vertex AI User. אם לזהות שלכם יש תפקיד 'בעלים', כבר יש לכם את תפקיד המשתמש הזה ב-Vertex AI. אם לא, תוכלו להריץ את הפקודה הזו כדי להקצות לזהות שלכם את תפקיד המשתמש ב-Vertex AI ואת התפקיד 'משתמש ב-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

לאחר מכן מריצים את הפקודה הבאה

gcloud auth application-default login

אפשרות 2) התחזות לחשבון שירות ל-ADC

אם אתם רוצים להשתמש בחשבון השירות שנוצר ב-Codelab הזה, לחשבון המשתמש שלכם צריך להיות התפקיד 'יצירת אסימונים בחשבון שירות'. כדי לקבל את התפקיד הזה, מריצים את הפקודה הבאה:

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

בשלב הבא, מריצים את הפקודה הבאה כדי להשתמש ב-ADC עם חשבון השירות

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

הפעלה מקומית של האפליקציה

לסיום, אפשר להפעיל את האפליקציה על ידי הרצת הסקריפט הבא. סקריפט הפיתוח הזה יפיק גם את הקובץoutput.css מ-tailwindCSS.

npm run dev

כדי לראות תצוגה מקדימה של האתר, אפשר לפתוח את לחצן Web Preview ולבחור באפשרות Preview Port 8080

תצוגה מקדימה לאינטרנט – תצוגה מקדימה בלחצן ליציאה 8080

8. פריסה ובדיקה של שירות Frontend

קודם כול מריצים את הפקודה הזו כדי להתחיל את הפריסה ולציין את חשבון השירות שבו רוצים להשתמש. אם לא מציינים חשבון שירות, ייעשה שימוש בחשבון השירות שמשמש כברירת מחדל ב-Compute Engine.

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

פותחים את כתובת ה-URL של השירות עבור ממשק הקצה בדפדפן. אפשר לשאול "What is the current weather in Seattle? " ו-Gemini צריך להשיב "עכשיו 40 מעלות וגשם". אם שואלים "What is the current weather in Boston? ", התשובה של Gemini היא "אין לי אפשרות למלא את הבקשה הזו. ה-API הזמין של מזג האוויר לא מכיל נתונים עבור בוסטון."

9. מעולה!

מזל טוב, השלמת את Codelab!

מומלץ לעיין במסמכים בנושא Cloud Run, Vertex AI Gemini APIs ושליחת קריאות לפעולות.

אילו נושאים דיברנו?

  • הסבר על שליחת פונקציות של Gemini
  • איך פורסים אפליקציית צ'אט בוט שמבוססת על Gemini בתור שירות Cloud Run

10. הסרת המשאבים

כדי להימנע מחיובים לא מכוונים (למשל, אם השירות הזה של Cloud Run מופעל בטעות יותר פעמים מהקצאת ההפעלה החודשית של Cloud Run בתוכנית ללא תשלום), אפשר למחוק את השירות Cloud Run או למחוק את הפרויקט שיצרתם בשלב 2.

כדי למחוק את שירותי Cloud Run, עוברים אל Cloud Run Cloud Console בכתובת https://console.cloud.google.com/functions/ ומוחקים את השירותים $WEATHER_SERVICE ו-$FRONTEND שיצרתם ב-Codelab הזה.

כדאי גם למחוק את חשבון השירות vertex-ai-caller או לבטל את התפקיד Vertex AI User כדי למנוע קריאות לא מכוונות ל-Gemini.

אם בוחרים למחוק את הפרויקט כולו, נכנסים לכתובת https://console.cloud.google.com/cloud-resource-manager, בוחרים את הפרויקט שיצרתם בשלב 2 ובוחרים באפשרות 'מחיקה'. אם תמחקו את הפרויקט, יהיה צריך לבצע שינויים בפרויקטים ב-Cloud SDK. כדי להציג את הרשימה של כל הפרויקטים הזמינים, אפשר להריץ את הפקודה gcloud projects list.