Agente di IA generativa per l'automazione degli ordini di vendita

1. Panoramica

Ultimo aggiornamento: 18/10/2024

Scritto da Sanggyu Lee (sanggyulee@google.com)

Cosa creerai

In questo codelab, creerai un agente IA generativa per i clienti di vendita al dettaglio

La tua app sarà in grado di:

  • Funziona su dispositivi mobili o computer.
  • Puoi scattare una foto di un articolo e ordinarlo con la chat vocale.
  • Ecco come funziona l'app: basta scattare una foto dell'articolo e dire, ad esempio, "Voglio ordinare questo prodotto, 3 scatole. Sono il direttore del Walmart di Honolulu." L'app carica la foto su Cloud Storage e trascrivi la registrazione vocale. Queste informazioni vengono poi inviate a un modello Gemini su Vertex AI, che identifica l'articolo e il tuo negozio (Walmart Honolulu). Se la richiesta soddisfa i criteri dell'ordine di vendita, il sistema genera un ordine di vendita con un ID univoco.

c8333d8139d8764c.png

2. Obiettivi didattici

Cosa imparerai a fare

  • Come creare un agente AI con Vertex AI
  • Come inviare audio e ricevere una trascrizione del testo dal servizio API Speech-to-Text
  • Come eseguire il deployment dell'agente AI su Cloud Run

Questo codelab è incentrato sulle app di agenti di IA generativa con Gemini. Concetti e blocchi di codice non pertinenti sono trattati solo superficialmente e sono forniti solo per operazioni di copia e incolla.

Che cosa ti serve

  • Account Google Cloud
  • Conoscenza di Python, JavaScript e Google Cloud

Architettura

b21e2a3deedb60ec.png

Questo agente è destinato a semplificare gli ordini utilizzando le funzionalità multimodali di Gemini con prompt di testo e immagini. Se l'ordine viene pronunciato, il modello Chirp 2 di Google Speech lo trascrive in testo, che viene poi utilizzato, insieme a un'immagine fornita, per eseguire query sul modello Gemini di Vertex AI.

Creeremo :

  1. Crea un ambiente di sviluppo
  2. App Flask da chiamare dagli utenti tramite dispositivo mobile o PC. L'app verrà eseguita su Cloud Run.

3. Configurazione e requisiti

Configurazione dell'ambiente a tuo ritmo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

fbef9caa1602edd0.png

97bdebccea2ba4be.png

3e14a8a504bb53ce.png

  • Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. Si tratta di una stringa di caratteri non utilizzata dalle API di Google. Puoi sempre aggiornarlo.
  • L'ID progetto è univoco per tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo essere stato impostato). La console Cloud genera automaticamente una stringa univoca; di solito non è importante quale sia. Nella maggior parte dei codelab, dovrai fare riferimento al tuo ID progetto (in genere identificato come PROJECT_ID). Se l'ID generato non ti piace, puoi generarne un altro casuale. In alternativa, puoi provare il tuo e vedere se è disponibile. Non può essere modificato dopo questo passaggio e rimane invariato per tutta la durata del progetto.
  • Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, dovrai abilitare la fatturazione nella console Cloud per utilizzare le API/risorse Cloud. La partecipazione a questo codelab non ha costi, o quasi. Per arrestare le risorse ed evitare di incorrere in fatturazione al termine di questo tutorial, puoi eliminare le risorse che hai creato o il progetto. I nuovi utenti di Google Cloud sono idonei al programma Prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere utilizzato da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Nella console Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

55efc1aaa7a4d3ad.png

Dovrebbe richiedere solo pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere qualcosa di simile a questo:

7ffe5cbb04455448.png

Questa macchina virtuale contiene tutti gli strumenti di sviluppo di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione di rete. Tutto il lavoro in questo codelab può essere svolto in un browser. Non devi installare nulla.

4. Prima di iniziare

Abilita API

Abilita le API richieste per il lab. L'operazione richiede alcuni minuti.

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

Output della console previsto :

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

Configura gli ambienti

Prima del comando CLI, configura i parametri per gli ambienti 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 la tua infrastruttura

Crea la rete per la tua app

Crea un VPC per la tua app. Per creare il VPC con il nome "demonetwork", esegui :

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

Per creare la subnet "genai-subnet" con intervallo di indirizzi 10.10.0.0/24 nella rete "demonetwork", esegui:

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

Crea un Cloud SQL per PostgreSQL

Intervalli di indirizzi IP allocati per l'accesso ai servizi privati.

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

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

Esegui il comando gcloud sql instances create per creare un'istanza 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

Il completamento di questo comando potrebbe richiedere alcuni minuti.

Output della console previsto :

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 un database per l'app e l'utente

Esegui il comando gcloud sql databases create per creare un database Cloud SQL in sql-retail-genai.

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

Crea un utente del database PostgreSQL, ti consigliamo di cambiare la password.

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

Creare un bucket per l'archiviazione delle immagini

Creare un bucket privato per l'agente

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

Aggiorna le autorizzazioni del bucket

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

Se si presume di utilizzare l'account di servizio Compute predefinito :

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

6. Preparare i codici per l'app

Prepara i codici

L'applicazione web per l'inserimento degli ordini è creata utilizzando Flask e può essere eseguita in un browser web su dispositivo mobile o PC. Accede al microfono e alla videocamera del dispositivo connesso e utilizza il modello Chirp 2 di Google Speech e il modello Gemini Pro 1.5 di Vertex AI. I risultati dell'ordine vengono archiviati in un database Cloud SQL.

Se hai utilizzato i nomi di variabili di ambiente di esempio forniti nella pagina precedente, puoi utilizzare il codice riportato di seguito senza modifiche. Se hai personalizzato i nomi delle variabili di ambiente, dovrai modificare di conseguenza alcuni valori delle variabili nel codice.

Crea due directory come segue.

mkdir -p genai-agent/templates

Crea un file requirements.txt

vi ~/genai-agent/requirements.txt

Inserisci un elenco di pacchetti nel file di testo.

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 file main.py

vi ~/genai-agent/main.py

Inserisci il codice Python nel file 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()

Creare index.html

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

Inserisci il codice HTML nel file 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 file orderlist.html

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

Inserisci il codice HTML nel file 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. Esegui il deployment dell'app Flask in Cloud Run

Dalla directory genai-agent, utilizza il seguente comando per eseguire il deployment dell'app in 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

Risultato previsto :

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

L'operazione richiederà alcuni minuti e, se viene completata correttamente, vedrai l'URL del servizio.

Risultato previsto :

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

Puoi anche controllare l'URL del servizio nella console Cloud Run.

8. Test

  1. Digita l'URL del servizio generato nel passaggio precedente del deployment di Cloud Run sul tuo dispositivo mobile o laptop.
  2. Scatta una foto di un articolo per il tuo ordine e inserisci la quantità dell'ordine(scatole) e il nome del negozio di vendita al dettaglio digitando o usando i comandi vocali. <ex> "Voglio ordinare queste tre scatole. No, scusa, sette scatole. Sono Walmart di Mountain View"
  3. Fai clic su "Invia" e controlla se l'ordine è stato completato.
  4. Puoi controllare la cronologia ordini all'indirizzo {Service URL}/orderlist

de0db1a08082c634.png

9. Complimenti

Complimenti! Hai creato un agente IA generativa in grado di automatizzare i processi aziendali utilizzando la multimodalità di Gemini su Vertex AI.

Sono felice che tu possa modificare i prompt e personalizzare l'agente in base alle tue esigenze specifiche.