Agente de IA generativa para la automatización de pedidos de venta

1. Descripción general

Última actualización: 18/10/2024

Escrito por Sanggyu Lee (sanggyulee@google.com)

Qué compilarás

En este codelab, crearás un agente de IA generativa para clientes minoristas.

Tu app hará lo siguiente:

  • Funciona en dispositivos móviles o computadoras.
  • Puedes tomar una foto de un artículo y pedirlo con el chat de voz.
  • La app funciona de la siguiente manera: solo debes tomar una foto del artículo y decir, por ejemplo, "Quiero pedir esto, 3 cajas. Soy el gerente de Walmart Honolulu." La app sube la foto a Cloud Storage y transcribe tu grabación de voz. Luego, esta información se envía a un modelo de Gemini en Vertex AI, que identifica el artículo y tu tienda (Walmart Honolulu). Si la solicitud cumple con los criterios del pedido de venta, el sistema genera un pedido de venta con un ID único.

c8333d8139d8764c.png

2. Qué aprenderás

Qué aprenderás

  • Cómo crear un agente de IA con Vertex AI
  • Cómo enviar audios y recibir una transcripción de texto del servicio de la API de Speech-to-Text
  • Cómo implementar tu agente de IA en Cloud Run

Este codelab se enfoca en las apps de agentes de IA generativa con Gemini. Los conceptos y los bloques de código no relevantes se pasan por alto y se proporcionan para que simplemente los copies y pegues.

Requisitos

  • Cuenta de Google Cloud
  • Conocimientos de Python, JavaScript y Google Cloud

Arquitectura

b21e2a3deedb60ec.png

Este agente se usa para simplificar los pedidos con las funciones de modalidad múltiple de Gemini con instrucciones de imagen y texto. Si el pedido se habla, el modelo Chirp 2 de Google Speech lo transcribe a texto, que luego se usa, junto con una imagen proporcionada, para consultar el modelo Gemini de Vertex AI.

Compilaremos lo siguiente :

  1. Crea un entorno de desarrollo
  2. Es la app de Flask a la que los usuarios llamarán a través de dispositivos móviles o PCs. La app se ejecutará en Cloud Run.

3. Configuración y requisitos

Cómo configurar el entorno a tu propio ritmo

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

fbef9caa1602edd0.png

97bdebccea2ba4be.png

3e14a8a504bb53ce.png

  • El nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla cuando quieras.
  • El ID del proyecto es único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. Por lo general, no importa cuál sea. En la mayoría de los codelabs, deberás hacer referencia al ID de tu proyecto (suele identificarse como PROJECT_ID). Si no te gusta el ID generado, puedes generar otro aleatorio. También puedes probar uno propio y ver si está disponible. No se puede cambiar después de este paso y se usa el mismo durante todo el proyecto.
  • Recuerda que hay un tercer valor, un número de proyecto, que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no costará mucho, tal vez nada. Para cerrar recursos y evitar que se generen cobros más allá de este instructivo, puedes borrar los recursos que creaste o borrar el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $300.

Inicie Cloud Shell

Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

En Google Cloud Console, haz clic en el ícono de Cloud Shell en la barra de herramientas en la parte superior derecha:

55efc1aaa7a4d3ad.png

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

7ffe5cbb04455448.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Todo tu trabajo en este codelab se puede hacer en un navegador. No es necesario que instales nada.

4. Antes de comenzar

Habilita las APIs

Habilita las APIs necesarias para el lab. Esto tardará algunos minutos.

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

Resultado esperado de la consola :

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

Configura los entornos

Antes de que el comando de CLI configure los parámetros de los entornos de Google Cloud.

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}

For example :

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. Crea tu infraestructura

Crea la red para tu app

Crea una VPC para tu app. Para crear la VPC con el nombre "demonetwork", ejecuta lo siguiente :

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

Para crear la subred "genai-subnet" con el rango de direcciones 10.10.0.0/24 en la red "demonetwork", ejecuta lo siguiente:

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

Crea una instancia de Cloud SQL para PostgreSQL

Rangos de direcciones IP asignados para el acceso a servicios privados.

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

Crea una conexión privada.

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

Ejecuta el comando gcloud sql instances create para crear una instancia de 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

Este comando puede tardar unos minutos en completarse.

Resultado esperado de la consola :

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

Crea una base de datos para tu app y el usuario

Ejecuta el comando gcloud sql databases create para crear una base de datos de Cloud SQL en sql-retail-genai.

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

Crea un usuario de la base de datos de PostgreSQL. Te recomendamos que cambies la contraseña.

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

Crea un bucket para almacenar imágenes

Crea un bucket privado para tu agente

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

Actualiza los permisos del bucket

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

Si supones que usas tu cuenta de servicio de Compute predeterminada, haz lo siguiente :

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

6. Prepara los códigos para tu app

Prepara los códigos

La aplicación web para realizar pedidos se compila con Flask y se puede ejecutar en un navegador web en dispositivos móviles o PCs. Accede al micrófono y la cámara del dispositivo conectado y usa el modelo Chirp 2 de Google Speech y el modelo Gemini Pro 1.5 de Vertex AI. Los resultados de los pedidos se almacenan en una base de datos de Cloud SQL.

Si usaste los nombres de variables de entorno de ejemplo que se proporcionaron en la página anterior, puedes usar el siguiente código sin modificaciones. Si personalizaste los nombres de las variables de entorno, deberás cambiar algunos valores de las variables en el código según corresponda.

Crea dos directorios de la siguiente manera.

mkdir -p genai-agent/templates

Crea un archivo requirements.txt

vi ~/genai-agent/requirements.txt

Ingresa una lista de paquetes en el archivo de texto.

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

Crea un archivo main.py

vi ~/genai-agent/main.py

Ingresa el código de Python en el archivo 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()

Crea un archivo index.html

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

Ingresa el código HTML en el archivo 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>

Crea un archivo orderlist.html

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

Ingresa el código HTML en el archivo 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. Implementa la app de Flask en Cloud Run

Desde el directorio genai-agent, usa el siguiente comando para implementar la app en Cloud Run:

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

Resultado esperado :

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

Este proceso tardará unos minutos y verás la URL del servicio si se completa correctamente.

Resultado esperado :

..........
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

También puedes verificar la URL del servicio en la consola de Cloud Run.

8. Prueba

  1. En tu dispositivo móvil o laptop, escribe la URL del servicio que se generó en el paso anterior de la implementación de Cloud Run.
  2. Toma una foto de un artículo de tu pedido y escribe o di el nombre de la tienda minorista y la cantidad del pedido(en cajas). <ex> "Quiero pedir estas tres cajas. No, lo siento, son siete cajas. Soy Walmart Mountain View".
  3. Haz clic en "Enviar" y verifica si se completó el pedido.
  4. Puedes consultar el historial de pedidos en {Service URL}/orderlist

de0db1a08082c634.png

9. Felicitaciones

¡Felicitaciones! Compilaste un agente de IA generativa capaz de automatizar procesos empresariales con Gemini en la modalidad multimodal de Vertex AI.

Me complace que modifiques las instrucciones y adaptes el agente a tus necesidades específicas.