1. 概要
最終更新日: 2024 年 10 月 18 日
作成者: Sanggyu Lee(sanggyulee@google.com)
作成するアプリの概要
この Codelab では、小売店の顧客向けの GenAI エージェントを構築します。
作成するアプリの機能は次のとおりです。
- モバイルまたはパソコンで使用できます。
- 商品の写真を撮って、音声チャットで注文できます。
- アプリの使い方は次のとおりです。商品の写真を撮影し、「この商品を 3 箱注文したい。Walmart Honolulu のマネージャーです。」アプリは写真を Cloud Storage にアップロードし、音声録音を文字に変換します。この情報は Vertex AI の Gemini モデルに送信され、アイテムと店舗(Walmart Honolulu)が識別されます。リクエストが販売注文の条件を満たしている場合、システムは一意の ID を持つ販売注文を生成します。
2. 学習内容
学習内容
- Vertex AI で AI エージェントを作成する方法
- Speech-to-Text API サービスに音声を送信して文字変換されたテキストを受け取る方法
- Cloud Run に AI エージェントをデプロイする方法
この Codelab では、Gemini を使用した生成 AI エージェント アプリに焦点を当てます。関連のない概念やコードブロックについては詳しく触れず、コードはコピーして貼るだけの状態で提供されています。
必要なもの
- Google Cloud アカウント
- Python、JavaScript、Google Cloud に関する知識
アーキテクチャ
このエージェントは、画像とテキスト プロンプトを使用して Gemini のマルチモーダル機能を使用した簡単な注文を目的としています。注文が音声で入力された場合は、Google Speech の Chirp 2 モデルによって音声がテキストに変換され、提供された画像とともに Vertex AI の Gemini モデルにクエリされます。
次のことを行います。
- 開発環境を作成する
- ユーザーがモバイルまたは PC から呼び出す Flask アプリ。アプリは Cloud Run で実行されます。
3. 設定と要件
セルフペース型の環境設定
- Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。
- プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。いつでも更新できます。
- プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常は、この内容を意識する必要はありません。ほとんどの Codelab では、プロジェクト ID(通常は PROJECT_ID と識別されます)を参照する必要があります。生成された ID が気に入らない場合は、別の ID をランダムに生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ ID になります。
- なお、3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
- 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に請求が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクトを削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。
Cloud Shell の起動
Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。
Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。
プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。
この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働します。そのため、ネットワークのパフォーマンスと認証機能が大幅に向上しています。この Codelab での作業はすべて、ブラウザ内から実行できます。インストールは不要です。
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 Cloud Environments のパラメータを設定します。
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. インフラストラクチャを構築する
アプリのネットワークを作成する
アプリの VPC を作成します。demonetwork という名前の VPC を作成するには、次のコマンドを実行します。
gcloud compute networks create demonetwork \ --subnet-mode custom
ネットワーク「demonetwork」にアドレス範囲 10.10.0.0/24 のサブネットワーク「genai-subnet」を作成するには、次のコマンドを実行します。
gcloud compute networks subnets create genai-subnet \ --network demonetwork \ --region us-central1 \ --range 10.10.0.0/24
Cloud SQL for 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 コマンドを実行して、sql-retail-genai 内に Cloud SQL データベースを作成します。
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 を使用して構築されており、モバイルまたは PC のウェブブラウザで実行できます。接続されたデバイスのマイクとカメラにアクセスし、Google Speech の Chirp 2 モデルと Vertex AI の Gemini Pro 1.5 モデルを使用します。注文結果は Cloud SQL データベースに保存されます。
前のページで示した環境変数名の例を使用した場合は、以下のコードを変更せずに使用できます。環境変数の名前をカスタマイズした場合は、コード内の一部の変数値を適宜変更する必要があります。
次のように 2 つのディレクトリを作成します。
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
main.py ファイルに Python コードを入力します。
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
index.html ファイルに 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
orderlist.html ファイルに 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 にデプロイする
genai-agent ディレクトリから、次のコマンドを使用してアプリを 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
期待される出力 :
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
完了するまでに数分かかります。正常に完了すると、Service 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
Cloud Run コンソールでサービス URL を確認することもできます。
8. テスト
- モバイルまたはラップトップに、Cloud Run のデプロイの前の手順で生成された Service URL を入力します。
- 注文する商品の写真を撮影し、注文数量(箱)と販売店名を入力または音声入力します。<ex> 「この 3 箱を注文したいのですが。申し訳ございません。7 箱です。This is Walmart Mountain Vew」
- [送信] をクリックして、注文が完了したかどうかを確認します。
- 注文履歴は {サービス URL}/orderlist で確認できます。
9. 完了
これで、Vertex AI のマルチモーダルで Gemini を使用してビジネス プロセスを自動化できる GenAI エージェントを構築しました。
プロンプトを変更して、特定のニーズに合わせてエージェントをカスタマイズしていただければ幸いです。