Агент GenAI для автоматизации заказов на продажу

1. Обзор

Последнее обновление: 18.10.2024

Автор: Сангю Ли (sanggyulee@google.com)

Что вы построите

В этом практическом занятии вы создадите агента GenAI для розничных клиентов.

Ваше приложение будет:

  • Работает как на мобильных устройствах, так и на компьютерах.
  • Вы можете сфотографировать товар и заказать его с помощью голосового чата.
  • Вот как работает приложение: вы просто фотографируете товар и говорите, например: « Я хочу заказать это, 3 коробки. Я менеджер в Walmart Honolulu». Приложение загружает фотографию в облачное хранилище и расшифровывает вашу голосовую запись. Затем эта информация отправляется модели Gemini на платформе Vertex AI, которая идентифицирует товар и ваш магазин (Walmart Honolulu). Если запрос соответствует критериям заказа на продажу, система генерирует заказ на продажу с уникальным идентификатором.

c8333d8139d8764c.png

2. Чему вы научитесь

Что вы узнаете

  • Как создать ИИ-агента с помощью Vertex AI
  • Как отправить аудиозапись и получить текстовую расшифровку из сервиса Speech-to-Text API
  • Как развернуть вашего ИИ-агента в Cloud Run

Данный практический урок посвящен приложениям на основе агентов GenAI с использованием Gemini. Несущественные концепции и блоки кода опущены и предоставлены для простого копирования и вставки.

Что вам понадобится

  • Аккаунт Google Cloud
  • Знание Python, Javascript и Google Cloud.

Архитектура

b21e2a3deedb60ec.png

Этот агент предназначен для упрощения процесса заказа с использованием многомодальных функций Gemini, включая подсказки в виде изображений и текста. Если заказ произносится вслух, модель Chirp 2 от Google Speech преобразует его в текст, который затем используется вместе с предоставленным изображением для запроса к модели Gemini от Vertex AI.

Мы построим:

  1. Создайте среду разработки.
  2. Приложение Flask будет вызываться пользователями с мобильных устройств или ПК. Приложение будет работать на платформе Cloud Run.

3. Настройка и требования

Настройка среды для самостоятельного обучения

  1. Войдите в консоль Google Cloud и создайте новый проект или используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .

fbef9caa1602edd0.png

97bdebccea2ba4be.png

3e14a8a504bb53ce.png

  • Название проекта — это отображаемое имя участников данного проекта. Это строка символов, не используемая API Google. Вы всегда можете его изменить.
  • Идентификатор проекта уникален для всех проектов Google Cloud и является неизменяемым (его нельзя изменить после установки). Консоль Cloud автоматически генерирует уникальную строку; обычно вам неважно, какая она. В большинстве практических заданий вам потребуется указать идентификатор вашего проекта (обычно обозначается как PROJECT_ID). Если сгенерированный идентификатор вас не устраивает, вы можете сгенерировать другой случайный идентификатор. В качестве альтернативы вы можете попробовать свой собственный и посмотреть, доступен ли он. После этого шага его нельзя изменить, и он сохраняется на протяжении всего проекта.
  • К вашему сведению, существует третье значение — номер проекта, которое используется некоторыми API. Подробнее обо всех трех значениях можно узнать в документации .
  1. Далее вам потребуется включить оплату в консоли Cloud для использования ресурсов/API Cloud. Выполнение этого практического задания не потребует больших затрат, если вообще потребует. Чтобы отключить ресурсы и избежать дополнительных расходов после завершения этого урока, вы можете удалить созданные ресурсы или удалить проект. Новые пользователи Google Cloud имеют право на бесплатную пробную версию стоимостью 300 долларов США .

Запустить Cloud Shell

Хотя Google Cloud можно управлять удаленно с ноутбука, в этом практическом занятии вы будете использовать Google Cloud Shell — среду командной строки, работающую в облаке.

В консоли Google Cloud нажмите на значок Cloud Shell на панели инструментов в правом верхнем углу:

55efc1aaa7a4d3ad.png

Подготовка и подключение к среде займут всего несколько минут. После завершения вы должны увидеть что-то подобное:

7ffe5cbb04455448.png

Эта виртуальная машина содержит все необходимые инструменты разработки. Она предоставляет постоянный домашний каталог объемом 5 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Вся работа в этом практическом задании может выполняться в браузере. Вам не нужно ничего устанавливать.

4. Прежде чем начать

Включить API

Включите API, необходимые для работы в лаборатории. Это займет несколько минут.

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  aiplatform.googleapis.com \
  speech.googleapis.com \
  sqladmin.googleapis.com \
  logging.googleapis.com \
  compute.googleapis.com \
  servicenetworking.googleapis.com \
  monitoring.googleapis.com

Ожидаемый вывод в консоль:

Operation "operations/acf.p2-639929424533-ffa3a09b-7663-4b31-8f78-5872bf4ad778" finished successfully.

Настройка среды

Перед выполнением команды CLI необходимо настроить параметры для облачных сред Google.

export PROJECT_ID="<YOUR_PROJECT_ID>"
export VPC_NAME="<YOUR_VPC_NAME>" e.g : demonetwork
export SUBNET_NAME="<YOUR_SUBNET_NAME>" e.g : genai-subnet
export REGION="<YOUR_REGION>" e.g : us-central1
export GENAI_BUCKET="<YOUR BUCKET FOR AGENT>" # eg> genai-${PROJECT_ID}

Например :

export PROJECT_ID=$(gcloud config get-value project)
export VPC_NAME="demonetwork" 
export SUBNET_NAME="genai-subnet" 
export REGION="us-central1" 
export GENAI_BUCKET="genai-${PROJECT_ID}" 

5. Создайте свою инфраструктуру

Создайте сеть для своего приложения.

Создайте VPC для вашего приложения. Чтобы создать VPC с именем "demonetwork", выполните следующую команду:

gcloud compute networks create demonetwork \
    --subnet-mode custom

Для создания подсети "genai-subnet" с диапазоном адресов 10.10.0.0/24 в сети "demonetwork" выполните следующую команду:

gcloud compute networks subnets create genai-subnet \
    --network demonetwork \
    --region us-central1 \
    --range 10.10.0.0/24

Создайте Cloud SQL для PostgreSQL

Выделенные диапазоны IP-адресов для доступа к частным сервисам.

gcloud compute addresses create google-managed-services-my-network \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=16 \
    --description="peering range for Google" \
    --network=demonetwork

Создайте приватное соединение.

gcloud services vpc-peerings connect \
    --service=servicenetworking.googleapis.com \
    --ranges=google-managed-services-my-network \
    --network=demonetwork

Выполните команду gcloud sql instances create , чтобы создать экземпляр Cloud SQL.

gcloud sql instances create sql-retail-genai \
  --database-version POSTGRES_14 \
  --tier db-f1-micro \
  --region=$REGION \
  --project=$PROJECT_ID \
  --network=projects/${PROJECT_ID}/global/networks/${VPC_NAME} \
  --no-assign-ip \
  --enable-google-private-path

Выполнение этой команды может занять несколько минут.

Ожидаемый вывод в консоль:

Created [https://sqladmin.googleapis.com/sql/v1beta4/projects/evident-trees-438609-q3/instances/sql-retail-genai].
NAME: sql-retail-genai
DATABASE_VERSION: POSTGRES_14
LOCATION: us-central1-c
TIER: db-f1-micro
PRIMARY_ADDRESS: -
PRIVATE_ADDRESS: 10.66.0.3
STATUS: RUNNABLE

Создайте базу данных для вашего приложения и пользователя.

Выполните команду gcloud sql databases create , чтобы создать базу данных Cloud SQL в рамках проекта sql-retail-genai.

gcloud sql databases create retail-orders \
  --instance sql-retail-genai

Создайте пользователя базы данных PostgreSQL, лучше всего сменить пароль.

gcloud sql users create aiagent --instance sql-retail-genai --password "genaiaigent2@"

Создайте хранилище для изображений.

Создайте приватный контейнер для своего агента.

gsutil mb -l $REGION gs://$GENAI_BUCKET

Обновить права доступа к хранилищу.

gsutil iam ch serviceAccount:<your service account>: roles/storage.objectUser gs://$GENAI_BUCKET

Если предполагается использование вашей учетной записи службы вычислений по умолчанию:

gsutil iam ch serviceAccount:$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")-compute@developer.gserviceaccount.com:roles/storage.objectUser gs://$GENAI_BUCKET

6. Подготовьте код для вашего приложения.

Подготовьте коды

Веб-приложение для оформления заказов создано с использованием Flask и может запускаться в веб-браузере на мобильном устройстве или ПК. Оно использует микрофон и камеру подключенного устройства, а также модель Chirp 2 от Google Speech и модель Gemini Pro 1.5 от Vertex AI. Результаты оформления заказов сохраняются в базе данных Cloud SQL.

Если вы использовали примеры имен переменных окружения, приведенные на предыдущей странице, вы можете использовать приведенный ниже код без изменений. Если у вас есть собственные имена переменных окружения, вам потребуется соответствующим образом изменить значения некоторых переменных в коде.

Создайте две директории следующим образом.

mkdir -p genai-agent/templates

Создайте файл requirements.txt.

vi ~/genai-agent/requirements.txt

Введите список пакетов в текстовый файл.

aiofiles==24.1.0
aiohappyeyeballs==2.4.3
aiohttp==3.10.9
aiosignal==1.3.1
annotated-types==0.7.0
asn1crypto==1.5.1
attrs==24.2.0
blinker==1.8.2
cachetools==5.5.0
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
click==8.1.7
cloud-sql-python-connector==1.12.1
cryptography==43.0.1
docstring_parser==0.16
Flask==3.0.3
frozenlist==1.4.1
google-api-core==2.20.0
google-auth==2.35.0
google-cloud-aiplatform==1.69.0
google-cloud-bigquery==3.26.0
google-cloud-core==2.4.1
google-cloud-resource-manager==1.12.5
google-cloud-speech==2.27.0
google-cloud-storage==2.18.2
google-crc32c==1.6.0
google-resumable-media==2.7.2
googleapis-common-protos==1.65.0
greenlet==3.1.1
grpc-google-iam-v1==0.13.1
grpcio==1.66.2
grpcio-status==1.66.2
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.0
multidict==6.1.0
numpy==2.1.2
packaging==24.1
pg8000==1.31.2
pgvector==0.3.5
proto-plus==1.24.0
protobuf==5.28.2
pyasn1==0.6.1
pyasn1_modules==0.4.1
pycparser==2.22
pydantic==2.9.2
pydantic_core==2.23.4
python-dateutil==2.9.0.post0
requests==2.32.3
rsa==4.9
scramp==1.4.5
shapely==2.0.6
six==1.16.0
SQLAlchemy==2.0.35
typing_extensions==4.12.2
urllib3==2.2.3
Werkzeug==3.0.4
yarl==1.13.1

Создайте файл main.py.

vi ~/genai-agent/main.py

Вставьте код Python в файл main.py.

from flask import Flask, render_template, request, jsonify, Response
import os
import base64
from google.api_core.client_options import ClientOptions
from google.cloud.speech_v2 import SpeechClient
from google.cloud.speech_v2.types import cloud_speech

import vertexai
from vertexai.generative_models import GenerativeModel, Part, SafetySetting
from google.cloud import storage
import uuid  # Import the uuid module
from typing import Dict  # Add this import
import datetime
import json
import re

import os
from google.cloud.sql.connector import Connector
import pg8000
import sqlalchemy
from sqlalchemy import create_engine, text

app = Flask(__name__)

# Replace with your actual project ID
project_id = os.environ.get("PROJECT_ID")

# Use a connection pool to reuse connections and improve performance
# This also handles connection lifecycle management automatically
engine = None

# Configure Google Cloud Storage
storage_client = storage.Client()
bucket_name = os.environ.get("GENAI_BUCKET")  
client = SpeechClient(
    client_options=ClientOptions(
        api_endpoint="us-central1-speech.googleapis.com",
    ),
)

def get_engine():
    global engine  # Use global to access/modify the global engine variable
    if engine is None:  # Create the engine only once
        connector = Connector()

        def getconn() -> pg8000.dbapi.Connection:
            conn: pg8000.dbapi.Connection = connector.connect(
                os.environ["INSTANCE_CONNECTION_NAME"],  # Cloud SQL instance connection name
                "pg8000",
                user=os.environ["DB_USER"],
                password=os.environ["DB_PASS"],
                db=os.environ["DB_NAME"],
                ip_type="PRIVATE",
            )
            return conn

        engine = create_engine(
            "postgresql+pg8000://",
            creator=getconn,
            pool_pre_ping=True,  # Check connection validity before use
            pool_size=5,  # Adjust pool size as needed
            max_overflow=2, #  Allow some overflow for bursts
            pool_recycle=300, #  Recycle connections after 5 minutes
        )
    return engine

def migrate_db() -> None:
    engine = get_engine()  # Get the engine (creates it if necessary)
    with engine.begin() as conn:
        sql = """
            CREATE TABLE IF NOT EXISTS image_sales_orders (
                order_id SERIAL PRIMARY KEY,
                vendor_name VARCHAR(80) NOT NULL,
                order_item VARCHAR(100) NOT NULL,
                order_boxes INT NOT NULL,  
                time_cast TIMESTAMP NOT NULL
            );
        """
        conn.execute(text(sql))


@app.before_request
def init_db():
    migrate_db()
    #print("Migration complete.")

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/orderlist')
def orderlist():
    engine = get_engine()
    with engine.connect() as conn:
        sql = text("""
            SELECT order_id, vendor_name, order_item, order_boxes, time_cast
            FROM image_sales_orders
            ORDER BY time_cast DESC
        """)
        result = conn.execute(sql).mappings()  # Use .mappings() for dict-like access
        orders = []
        for row in result:
            order = {
                'OrderId': row['order_id'],
                'VendorName': row['vendor_name'],
                'OrderItem': row['order_item'],
                'OrderBoxes': row['order_boxes'],
                'OrderDate': row['time_cast'].strftime('%Y-%m-%d'),
                'OrderTime': row['time_cast'].strftime('%H:%M:%S'),
            }
            orders.append(order)
    return render_template('orderlist.html', orders=orders)

@app.route("/upload_photo", methods=["POST"])
def upload_photo():
    # Get the uploaded file
    file = request.files["photo"]

    # Generate a unique filename
    filename = f"{uuid.uuid4()}--{file.filename}"

    # Upload the file to Google Cloud Storage
    bucket = storage_client.get_bucket(bucket_name)
    blob = bucket.blob(filename)
    generation_match_precondition = 0
    blob.upload_from_file(file, if_generation_match=generation_match_precondition)

    # Return the destination filename
    image_url = f"gs://{bucket_name}/{filename}"

    # Return the destination filename
    return image_url

@app.route('/upload', methods=['POST'])
def upload():
    audio_data = request.form['audio_data']
    audio_data = base64.b64decode(audio_data.split(',')[1])

    audio_path = f"{uuid.uuid4()}--audio.wav" 

    with open(audio_path, 'wb') as f:
        f.write(audio_data)

    transcript = transcribe_speech(audio_path)
    os.remove(audio_path)
    return jsonify({'transcript': transcript})

@app.route("/orders", methods=["POST"])
def cast_order() -> Response:
    prompt = request.form['transcript']
    image_url = request.form['image_url']
    print(f"Prompt: {prompt}")
    print(f"Image URL: {image_url}")

    model_response = generate(image_url=image_url, prompt=prompt)
    # Extract the text content from the model response
    response_text = model_response.text if hasattr(model_response, 'text') else str(model_response)

    #print(f"Response from Model !!!!!!: {response_text}")

    try:
        response_json = json.loads(response_text)
        function_name = response_json.get("function")
        parameters = response_json.get("parameters")

    except json.JSONDecodeError as e:
        logging.error(f"JSON decoding error: {e}")
        return Response(
            "I cannot fulfill your request because I cannot find the [Product Name], [Quantity (Box)], and [Retail Store Name] in the provided image and prompt.",
            status=500
        )

    if function_name == 'Z_SALES_ORDER_SRV/orderlistSet':
        engine = get_engine()
        with engine.connect() as conn:
            try:
                # Explicitly convert order_boxes to integer
                order_boxes = int(parameters["order_boxes"])
                vendor_name = parameters["vendor_name"]
                order_item = parameters["order_item"]

                # Prepare the SQL statement
                sql = text("""
                    INSERT INTO image_sales_orders (vendor_name, order_item, order_boxes, time_cast)
                    VALUES (:vendor_name, :order_item, :order_boxes, NOW())
                """)

                # Prepare parameters
                params = {
                    "vendor_name": vendor_name,
                    "order_item": order_item,
                    "order_boxes": order_boxes,
                }

                # Execute the SQL statement with parameters
                conn.execute(sql, params)
                conn.commit()

                response_message = f"Dear [{vendor_name}],\n\nYour order has been completed as follows. \n\nItem Name : {order_item}\nQTY(Boxes) : {order_boxes}\n\nThanks."
                return Response(response_message, status=200)

            except (KeyError, ValueError) as e:
                logging.error(f"Error inserting into database: {e}")
                response_message = "Error processing your order. Please check the input data."
                return Response(response_message, status=500)

    else:
        # Handle other function names if necessary
        return Response("Unknown function.", status=400)


def transcribe_speech(audio_file):
    with open(audio_file, "rb") as f:
        content = f.read()

    config = cloud_speech.RecognitionConfig(
        auto_decoding_config=cloud_speech.AutoDetectDecodingConfig(),
        language_codes=["auto"],
        #language_codes=["ko-KR"],    -- In case that needs to choose specific language 
        model="chirp_2",
    )

    request = cloud_speech.RecognizeRequest(
        recognizer=f"projects/{project_id}/locations/us-central1/recognizers/_",
        config=config,
        content=content,
    )

    response = client.recognize(request=request)

    transcript = ""
    for result in response.results:
        transcript += result.alternatives[0].transcript

    return transcript

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
    #app.run(debug=True)

def generate(image_url,prompt):
    vertexai.init(project=project_id, location="us-central1")
    model = GenerativeModel("gemini-1.5-pro-002")
    image1 = Part.from_uri(uri=image_url, mime_type="image/jpeg")

    prompt_default = """A retail store will give you an image with order details as an Input. You will identify the order details and provide an output as the following json format. You should not add any comment on it. The Box quantity should be arabic number. You can extract the item name from a given image or prompt. However, you should extract the retail store name or the quantity from only the text prompt but not the given image. All parameter values are strings. Don't assume any parameters. Do not wrap the json codes in JSON markers.

{\"function\":\"Z_SALES_ORDER_SRV/orderlistSet\",\"parameters\":{\"vendor_name\":Retail store name,\"order_item\":Item name,\"order_boxes\":Box quantity}}

If you are not clear on any parameter, provide the output as follows.
{\"function\":\"None\"}

You should not use the json markdown for the result.

Input :"""

    generation_config = {
        "max_output_tokens": 8192,
        "temperature": 0,
        "top_p": 0.95,
    }

    safety_settings = [
        SafetySetting(
            category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
            threshold=SafetySetting.HarmBlockThreshold.OFF
        ),
        SafetySetting(
            category=SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
            threshold=SafetySetting.HarmBlockThreshold.OFF
        ),
        SafetySetting(
            category=SafetySetting.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
            threshold=SafetySetting.HarmBlockThreshold.OFF
        ),
        SafetySetting(
            category=SafetySetting.HarmCategory.HARM_CATEGORY_HARASSMENT,
            threshold=SafetySetting.HarmBlockThreshold.OFF
        ),
    ]

    responses = model.generate_content(
        [prompt_default, image1, prompt],
        generation_config=generation_config,
        safety_settings=safety_settings,
        stream=True,
    )

    response = ""
    for content in responses:
       response += content.text
       print(f"Content: {content}")
       print(f"Content type: {type(content)}")
       print(f"Content attributes: {dir(content)}")

    print(f"response_texts={response}")

    if response.startswith('json'):
       return clean_json_string(response)
    else:
       return response

def clean_json_string(json_string):
    pattern = r'^```json\s*(.*?)\s*```$'
    cleaned_string = re.sub(pattern, r'\1', json_string, flags=re.DOTALL)
    return cleaned_string.strip()

Создайте файл index.html

vi ~/genai-agent/templates/index.html

Вставьте HTML-код в файл index.html.

<!DOCTYPE html>
<html>
<head>
    <title>GenAI Agent for Retail</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* Styles adjusted for chatbot interface */
        body {
            font-family: Arial, sans-serif;
            background-color: #343541;
            margin: 0;
            padding: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
        }

        .chat-container {
            flex: 1;
            overflow-y: auto;
            padding: 10px;
            background-color: #343541;
        }

        .message {
            max-width: 80%;
            margin-bottom: 15px;
            padding: 10px;
            border-radius: 10px;
            color: #dcdcdc;
            word-wrap: break-word;
        }

        .user-message {
            background-color: #3e3f4b;
            align-self: flex-end;
        }

        .assistant-message {
            background-color: #444654;
            align-self: flex-start;
        }

        .message-input {
            padding: 10px;
            background-color: #40414f;
            display: flex;
            align-items: center;
        }

        .message-input textarea {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 5px;
            resize: none;
            background-color: #40414f;
            color: #dcdcdc;
            height: 40px;
            max-height: 100px;
            overflow-y: auto;
        }

        .message-input button {
            padding: 15px;
            margin-left: 5px;
            background-color: #19c37d;
            border: none;
            border-radius: 5px;
            color: white;
            font-weight: bold;
            cursor: pointer;
            flex-shrink: 0;
        }

        .image-preview {
            max-width: 100%;
            border-radius: 10px;
            margin-bottom: 10px;
        }

        .hidden {
            display: none;
        }

        /* Media queries for responsive design */
        @media screen and (max-width: 600px) {
            .message {
                max-width: 100%;
            }

            .message-input {
                flex-direction: column;
            }

            .message-input textarea {
                width: 100%;
                margin-bottom: 10px;
            }

            .message-input button {
                width: 100%;
                margin: 5px 0;
            }
        }
    </style>
</head>
<body>
    <div class="chat-container" id="chat-container">
        <!-- Messages will be appended here -->
    </div>

    <div class="message-input">
        <input type="file" name="photo" id="photo" accept="image/*" capture="camera" class="hidden">
        <button id="uploadImageButton">📷</button>
        <button id="recordButton">🎤</button>
        <textarea id="transcript" rows="1" placeholder="Enter a message here by voice or typing..."></textarea>
        <button id="sendButton">Send</button>
    </div>

    <script>
        const chatContainer = document.getElementById('chat-container');
        const transcriptInput = document.getElementById('transcript');
        const sendButton = document.getElementById('sendButton');
        const recordButton = document.getElementById('recordButton');
        const uploadImageButton = document.getElementById('uploadImageButton');
        const photoInput = document.getElementById('photo');

        let mediaRecorder;
        let audioChunks = [];
        let imageUrl = '';

        function appendMessage(content, sender) {
            const messageDiv = document.createElement('div');
            messageDiv.classList.add('message', sender === 'user' ? 'user-message' : 'assistant-message');

            if (typeof content === 'string') {
                const messageContent = document.createElement('p');
                messageContent.innerText = content;
                messageDiv.appendChild(messageContent);
            } else {
                messageDiv.appendChild(content);
            }

            chatContainer.appendChild(messageDiv);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }

        sendButton.addEventListener('click', () => {
            const message = transcriptInput.value.trim();
            if (message !== '') {
                appendMessage(message, 'user');

                // Prepare form data
                const formData = new FormData();
                formData.append('transcript', message);
                formData.append('image_url', imageUrl);

                // Send the message to the server
                fetch('/orders', {
                    method: 'POST',
                    body: formData
                })
                .then(response => response.text())
                .then(data => {
                    appendMessage(data, 'assistant');
                    // Reset imageUrl after sending
                    imageUrl = '';
                })
                .catch(error => {
                    console.error('Error:', error);
                });

                transcriptInput.value = '';
            }
        });

        transcriptInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendButton.click();
            }
        });

        recordButton.addEventListener('click', async () => {
            if (mediaRecorder && mediaRecorder.state === 'recording') {
                mediaRecorder.stop();
                recordButton.innerText = '🎤';
                return;
            }

            let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            mediaRecorder = new MediaRecorder(stream);
            mediaRecorder.start();
            recordButton.innerText = '⏹️';

            mediaRecorder.ondataavailable = event => {
                audioChunks.push(event.data);
            };

            mediaRecorder.onstop = async () => {
                let audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
                audioChunks = [];

                let reader = new FileReader();
                reader.readAsDataURL(audioBlob);
                reader.onloadend = () => {
                    let base64String = reader.result;

                    // Send the audio data to the server
                    fetch('/upload', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded'
                        },
                        body: 'audio_data=' + encodeURIComponent(base64String)
                    })
                    .then(response => response.json())
                    .then(data => {
                        transcriptInput.value = data.transcript;
                    })
                    .catch(error => {
                        console.error('Error:', error);
                    });
                };
            };
        });

        uploadImageButton.addEventListener('click', () => {
            photoInput.click();
        });

        photoInput.addEventListener('change', function() {
            if (photoInput.files && photoInput.files[0]) {
                const file = photoInput.files[0];
                const reader = new FileReader();
                reader.onload = function(e) {
                    const img = document.createElement('img');
                    img.src = e.target.result;
                    img.classList.add('image-preview');
                    appendMessage(img, 'user');
                };
                reader.readAsDataURL(file);

                const formData = new FormData();
                formData.append('photo', photoInput.files[0]);

                // Upload the image to the server
                fetch('/upload_photo', {
                    method: 'POST',
                    body: formData,
                })
                .then(response => response.text())
                .then(url => {
                    imageUrl = url;
                })
                .catch(error => {
                    console.error('Error uploading photo:', error);
                });
            }
        });
    </script>
</body>
</html>

Создайте файл orderlist.html

vi ~/genai-agent/templates/orderlist.html

Вставьте HTML-код в файл orderlist.html.

<!DOCTYPE html>
<html>
<head>
    <title>Order List</title>
    <style>
        body {
            font-family: sans-serif;
            line-height: 1.6;
            margin: 20px;
            background-color: #f4f4f4;
            color: #333;
        }

        h1 {
            text-align: center;
            color: #28a745; /* Green header */
        }

        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Add a subtle shadow */
        }

        th, td {
            padding: 12px 15px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }

        th {
            background-color: #28a745; /* Green header background */
            color: white;
        }

        tr:nth-child(even) {
            background-color: #f8f9fa; /* Alternating row color */
        }

        tr:hover {
            background-color: #e9ecef; /* Hover effect */
        }

    </style>
</head>
<body>
    <h1>Order List</h1>
    <table>
        <thead>
            <tr>
                <th>Order ID</th>
                <th>Retail Store Name</th>
                <th>Order Item</th>
                <th>Order Boxes</th>
                <th>Order Date</th>
                <th>Order Time</th>
            </tr>
        </thead>
        <tbody>
            {% for order in orders %}
            <tr>
                <td>{{ order.OrderId }}</td>
                <td>{{ order.VendorName }}</td>
                <td>{{ order.OrderItem }}</td>
                <td>{{ order.OrderBoxes }}</td>
                <td>{{ order.OrderDate }}</td>
                <td>{{ order.OrderTime }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

7. Разверните приложение Flask в Cloud Run.

Для развертывания приложения в Cloud Run используйте следующую команду из каталога genai-agent:

cd ~/genai-agent
gcloud run deploy --source . genai-agent-sales-order \
--set-env-vars=PROJECT_ID=$PROJECT_ID \
--set-env-vars=REGION=$REGION \
--set-env-vars=INSTANCE_CONNECTION_NAME="${PROJECT_ID}:${REGION}:sql-retail-genai" \
--set-env-vars=DB_USER=aiagent \
--set-env-vars=DB_PASS=genaiaigent2@ \
--set-env-vars=DB_NAME=retail-orders \
--set-env-vars=GENAI_BUCKET=$GENAI_BUCKET \
--network=$PROJECT_ID \
--subnet=$SUBNET_NAME \
--vpc-egress=private-ranges-only \
--region=$REGION \
--allow-unauthenticated

Ожидаемый результат:

Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named [cloud-run-source-deploy] in region [us-central1] will be created.

Do you want to continue (Y/n)?  Y

Это займет несколько минут, и вы увидите URL-адрес сервиса, если процесс завершится успешно.

Ожидаемый результат:

..........
Building using Buildpacks and deploying container to Cloud Run service [genai-agent-sales-order] in project [xxxx] region [us-central1]
✓ Building and deploying... Done.                                                                                                                                                                                                                                                                                                                               
  ✓ Uploading sources...                                                                                                                                                                                                                                                                                                                                        
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/395d141c-2dcf-465d-acfb-f97831c448c3?project=xxxx].                                                                                                                                                                                                
  ✓ Creating Revision...                                                                                                                                                                                                                                                                                                                                        
  ✓ Routing traffic...                                                                                                                                                                                                                                                                                                                                          
  ✓ Setting IAM Policy...                                                                                                                                                                                                                                                                                                                                       
Done.                                                                                                                                                                                                                                                                                                                                                           
Service [genai-agent-sales-order] revision [genai-agent-sales-order-00013-ckp] has been deployed and is serving 100 percent of traffic.
Service URL: https://genai-agent-sales-order-xxxx.us-central1.run.app

Вы также можете проверить URL-адрес сервиса в консоли Cloud Run.

8. Тест

  1. Введите URL-адрес службы, сгенерированный на предыдущем шаге развертывания в облаке, на своем мобильном устройстве или ноутбуке.
  2. Сфотографируйте товар для вашего заказа и введите количество (в коробках) и название розничного магазина, набрав текст или произнеся его голосом. Например: "Я хочу заказать три коробки. О нет, извините, семь коробок. Это Walmart Mountain View".
  3. Нажмите «Отправить» и проверьте, выполнен ли ваш заказ.
  4. Историю заказов можно посмотреть по адресу {Service URL}/orderlist

de0db1a08082c634.png

9. Поздравляем!

Поздравляем! Вы создали агента GenAI, способного автоматизировать бизнес-процессы, используя Gemini на многомодальной платформе Vertex AI.

Я с нетерпением жду, когда вы измените подсказки и настроите агента под свои конкретные потребности.