1. Giới thiệu
Tổng quan
Trong lớp học lập trình này, bạn sẽ xây dựng một trợ lý AI cá nhân giúp bạn phân tích dữ liệu kinh doanh và thực hiện các tác vụ khác thông qua giao diện người dùng trò chuyện. Bạn sẽ sử dụng một dịch vụ Cloud Run để lưu trữ tác nhân cá nhân của mình.
Tác nhân của bạn sẽ sử dụng hộp cát Cloud Run. Hộp cát Cloud Run là một môi trường thời gian chạy gốc, an toàn và cực nhanh, được xây dựng dành riêng cho việc thực thi mã và khối lượng công việc của tác nhân không đáng tin cậy, bắt đầu trong vài mili giây. Hộp cát cho phép AI Agent của bạn viết, chạy và kiểm thử mã một cách linh hoạt ngay lập tức để giải quyết các vấn đề phân tích phức tạp.
Lưu ý: Để đảm bảo trải nghiệm phát triển liền mạch khi chạy cục bộ so với trong quá trình sản xuất:
- Trong quá trình phát hành công khai (hộp cát Cloud Run): Tác nhân chạy mã một cách an toàn bên trong một môi trường thử nghiệm biệt lập, được chứa trong vùng chứa thông qua một tệp nhị phân hộp cát chuyên dụng (/usr/local/gcp/bin/sandbox).
- Cục bộ (Máy của bạn): Khi chạy cục bộ, ứng dụng sẽ phát hiện thấy môi trường hộp cát phát hành chính thức không có (IS_LOCAL_MODE = True). Tác nhân thực thi các tập lệnh Python và lệnh shell ngay trên cửa sổ dòng lệnh của hệ thống máy chủ lưu trữ cục bộ.
Sản phẩm bạn sẽ tạo ra:
Trong trường hợp này, bạn quản lý một quán cà phê ở thị trấn đại học và đang chuẩn bị cho một dịp cuối tuần tốt nghiệp lớn. Bạn cần yêu cầu tác nhân của mình tham chiếu chéo dữ liệu thô tại Điểm bán hàng (POS) với lịch trình tổ chức lễ của trường đại học để phát hiện những điểm tắc nghẽn tiềm ẩn trong hoạt động.
Tác nhân này sử dụng một hộp cát an toàn để viết và thực thi các tập lệnh Python, phân tích độ phức tạp của đồ uống so với số lượng nhân viên thu ngân để đề xuất điều chỉnh nhân sự và hàng tồn kho.
Trợ lý sẽ gửi thông báo cho chủ sở hữu thông qua giao diện người dùng trò chuyện mô phỏng, kèm theo các đề xuất về nhân viên và kho hàng mục tiêu. Ứng dụng này chờ sự cho phép rõ ràng trước khi cập nhật bảng tính bằng các việc cần làm về hoạt động cho người quản lý quán cà phê.
Kiến thức bạn sẽ học được
- Cách tạo một dịch vụ Cloud Run
- Cách triển khai một tác nhân ADK trên dịch vụ Cloud Run
- Cách để một tác nhân chạy mã trong hộp cát trong một dịch vụ Cloud Run
- Cách tạo giao diện người dùng trò chuyện bằng WebSocket để tương tác với tác nhân nền
2. Thiết lập và yêu cầu
Định cấu hình mã dự án và khu vực.
GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
REGION=us-west2
gcloud config set project $GOOGLE_CLOUD_PROJECT
gcloud config set run/region $REGION
Sau đây là các biến môi trường sẽ được dùng trong suốt lớp học lập trình này. Bạn có thể lưu các biến này trong một tệp môi trường và "source" tệp đó. Đảm bảo bạn đặt đúng giá trị của mã dự án và khu vực (không bắt buộc).
SA_NAME=coffee-shop-agent-sa
SERVICE_ACCOUNT_ADDRESS=$SA_NAME@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com
Bật các API cần thiết cho Lớp học lập trình này
gcloud services enable --project $GOOGLE_CLOUD_PROJECT \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
sheets.googleapis.com \
aiplatform.googleapis.com
3. Tạo tài khoản dịch vụ và bảng tính
Bạn nên tạo một Tài khoản dịch vụ chuyên dụng cho dịch vụ Cloud Run để chỉ cấp các vai trò cần thiết.
Tạo tài khoản dịch vụ cho dịch vụ Cloud Run
gcloud iam service-accounts create $SA_NAME \
--description="Service account for the Coffee Shop Agent Codelab" \
--display-name="Coffee Shop Agent SA"
Vì tác nhân sử dụng Gemini API (GOOGLE_GENAI_USE_VERTEXAI=1), hãy cấp cho tài khoản dịch vụ này vai trò Người dùng nền tảng tác nhân trong dự án của bạn.
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
--member="serviceAccount:$SERVICE_ACCOUNT_ADDRESS" \
--role="roles/aiplatform.user"
Để cho phép bạn chạy và kiểm thử tác nhân cục bộ bằng tài khoản dịch vụ này, hãy cấp cho danh tính Google Cloud cá nhân của bạn quyền mạo danh tài khoản dịch vụ này:
gcloud iam service-accounts add-iam-policy-binding \
coffee-shop-agent-sa@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \
--member="user:$(gcloud config get-value account)" \
--role="roles/iam.serviceAccountTokenCreator"
Tạo bảng tính
Bảng tính này thể hiện doanh số bán hàng diễn ra vào cuối tuần tốt nghiệp năm ngoái.
- Tạo một tệp Google Trang tính mới trong Google Drive.
- Sao chép các Giá trị phân tách bằng dấu phẩy (CSV) sau đây vào Google Trang tính mới (ví dụ: chọn ô A1 rồi dán).
Day,Time,Drip_Coffee,Cold_Brew,Extra_Espresso,Alt_Milk_Oz,Pastries,Cashiers_Working,Wait_Time_Minutes
Saturday,07:00:00,95,15,5,30,80,2,9
Saturday,08:00:00,80,25,10,35,60,2,7
Saturday,09:00:00,30,30,20,40,20,1,4
Saturday,10:00:00,40,130,95,50,25,2,4
Saturday,11:00:00,25,45,25,40,15,1,5
Saturday,12:00:00,30,35,15,45,20,1,3
Saturday,13:00:00,15,20,10,30,10,1,2
Saturday,14:00:00,45,60,30,65,30,2,8
Saturday,15:00:00,20,30,15,35,15,1,3
Saturday,16:00:00,25,35,10,50,20,1,4
Saturday,17:00:00,10,20,5,35,10,1,2
Saturday,18:00:00,20,40,10,190,65,2,12
Saturday,19:00:00,10,15,5,40,15,1,2
Sunday,07:00:00,90,10,5,35,75,2,8
Sunday,08:00:00,75,20,10,40,65,2,6
Sunday,09:00:00,25,25,15,35,15,1,3
Sunday,10:00:00,60,35,15,60,50,2,7
Sunday,11:00:00,20,30,10,35,10,1,3
Sunday,12:00:00,25,40,10,55,20,1,3
Sunday,13:00:00,15,25,5,40,10,1,2
Sunday,14:00:00,30,50,15,180,60,2,11
Sunday,15:00:00,15,25,10,45,15,1,3
Sunday,16:00:00,20,45,20,40,15,1,4
Sunday,17:00:00,10,25,10,30,10,1,2
Sunday,18:00:00,25,145,110,45,20,2,16
Sunday,19:00:00,15,30,15,35,10,1,4
- khi dữ liệu của bạn vẫn được chọn, hãy nhấp vào Dữ liệu > Phân tách văn bản thành các cột trong trình đơn trên cùng của Google Trang tính.
Đảm bảo tài khoản dịch vụ có quyền truy cập của người chỉnh sửa đối với bảng tính. Cách này tương tự như cách bạn cấp quyền truy cập cho một thành viên trong nhóm.
- Sao chép toàn bộ email của tài khoản dịch vụ mới
echo $SERVICE_ACCOUNT_ADDRESS
- Trong Google Trang tính, hãy nhấp vào biểu tượng Chia sẻ ở góc trên cùng bên phải
- Dán email của tài khoản dịch vụ, đặt quyền thành Người chỉnh sửa rồi nhấp vào Chia sẻ. (Bạn có thể bỏ đánh dấu vào ô Thông báo).
- Ghi lại mã bảng tính mà bạn sẽ truyền cho tác nhân, ví dụ: https://docs.google.com/spreadsheets/d/<YOUR_SPREADSHEET_ID>/edit?gid=0#gid=0
SPREADSHEET_ID=<THE SPREADHSEET ID FROM ITS URL>
4. Tạo tác nhân ADK
Trước tiên, hãy tạo một thư mục cho mã của bạn.
mkdir coffee-mgr-agent && cd coffee-mgr-agent
Tạo tệp requirements.txt.
fastapi>=0.100.0
uvicorn>=0.22.0
google-adk>=1.27.1
google-auth
google-api-python-client
Tạo một Dockerfile
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Tạo tệp main.py
import os
import asyncio
import subprocess
from pathlib import Path
from typing import List
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from google.adk.agents import LlmAgent as Agent
from google.adk.tools import FunctionTool
from google.adk.apps import App
from google.adk import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
SANDBOX_CLI = '/usr/local/gcp/bin/sandbox'
IS_LOCAL_MODE = not Path(SANDBOX_CLI).exists()
active_connections: list[WebSocket] = []
SPREADSHEET_ID = os.environ.get("SPREADSHEET_ID")
# And
def run_sandbox_process(args: list[str]):
cmd = args[2:] if IS_LOCAL_MODE and args[:2] == ['do', '--'] else ([SANDBOX_CLI] + args if not IS_LOCAL_MODE else args)
return subprocess.run(cmd, capture_output=True, text=True, timeout=10)
def execute_sandbox_command(command: str) -> str:
"""Executes arbitrary POSIX shell/bash commands inside sandbox."""
mode = "LOCAL" if IS_LOCAL_MODE else "CLOUD RUN SANDBOX"
print(f"[ADK Sandbox Tool] Starting {mode} shell run...")
try:
res = run_sandbox_process(['do', '--', '/bin/sh', '-c', command])
if res.returncode != 0:
return f"Execution Failed!\nExit Code: {res.returncode}\nStdout:\n{res.stdout}\nStderr:\n{res.stderr}"
return res.stdout
except Exception as err:
return f"Internal Sandbox Tool Error: {str(err)}"
def get_sheets_service():
"""Initializes and returns the Google Sheets client service."""
from google.auth import default
from googleapiclient.discovery import build
credentials, _ = default(scopes=[
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/cloud-platform'
])
return build('sheets', 'v4', credentials=credentials)
def read_spreadsheet_values(spreadsheet_id: str, range_name: str) -> str:
"""Reads a range of cells from a Google Spreadsheet."""
try:
service = get_sheets_service()
result = service.spreadsheets().values().get(spreadsheetId=spreadsheet_id, range=range_name).execute()
rows = result.get('values', [])
return str(rows) if rows else "No data found in the specified range."
except Exception as e:
return f"Read Error: {str(e)}"
def update_spreadsheet_values(spreadsheet_id: str, range_name: str, values: List[List[str]]) -> str:
"""Updates a range of cells in a Google Spreadsheet with the provided values."""
try:
service = get_sheets_service()
result = service.spreadsheets().values().update(
spreadsheetId=spreadsheet_id, range=range_name,
valueInputOption="USER_ENTERED", body={'values': values}).execute()
return f"Successfully updated {result.get('updatedCells')} cells in {range_name}."
except Exception as e:
return f"Write Error: {str(e)}"
def create_spreadsheet_tab(spreadsheet_id: str, tab_name: str) -> str:
"""Creates a new sheet tab in a Google Spreadsheet if it doesn't already exist."""
try:
service = get_sheets_service()
# Check if tab exists
spreadsheet = service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
for sheet in spreadsheet.get('sheets', []):
if sheet.get('properties', {}).get('title') == tab_name:
return f"Sheet tab '{tab_name}' already exists."
# Create tab
body = {'requests': [{'addSheet': {'properties': {'title': tab_name}}}]}
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
return f"Successfully created new sheet tab '{tab_name}'."
except Exception as e:
return f"Error creating sheet tab: {str(e)}"
# ==========================================
# 3. ADK AGENT & RUNNER SETUP
# ==========================================
root_agent = Agent(
name='secure_coding_assistant',
description='ADK agent capable of executing shell commands and managing Google Spreadsheets.',
model=os.environ.get('GEMINI_MODEL', 'gemini-3.1-flash-lite'),
instruction=(
f'You are an expert AI Business Analyst for a coffee shop during university graduation weekend.\n'
f'The Google Spreadsheet ID you are managing is: "{SPREADSHEET_ID}". Use this ID for all sheet operations.\n'
'1. Comparative Analysis Policy:\n'
f' - Ingest historical POS data from the "POS-2025" sheet tab using read_spreadsheet_values with spreadsheet_id="{SPREADSHEET_ID}".\n'
' - Receive the current graduation schedule directly from the manager\'s prompt (the manager will paste it and indicate it is the same schedule sequence as last year).\n'
' - Write a python3 script via the sandbox tool to:\n'
' a. Correlate the 2025 product spikes (Cold Brew, Alt Milk, Extra Espresso) with the specific ceremonies ending at those times.\n'
' b. Map those beverage profiles to the pasted schedule (which is the same sequence) to predict exactly when and where the 2026 spikes will occur.\n'
' c. Identify expected wait-time bottlenecks in 2026 based on the 2025 wait times for those same profiles.\n'
'2. Bottleneck Diagnostics (Playbook):\n'
' - If a predicted high-volume slot in 2026 is expected to have Wait_Time_Minutes > 10:\n'
' - If Cashiers_Working < 2: Recommend scheduling another cashier.\n'
' - If Cashiers_Working == 2 and complex items (Cold Brew, Extra Espresso, Alt Milk) spike: Deduce that the bottleneck is barista output, not cashiers. Recommend adding a "Support Barista" role to handle fulfillment.\n'
'3. Human-in-the-Loop Policy:\n'
' - Present your detailed data discoveries, wait-time bottlenecks, and actionable recommendations (stocking and staffing changes) to the manager.\n'
' - Highlight only two or three findings for specific ceremonies.\n'
' - Frame your recommendations as a clean list of suggested tasks for the manager\'s TODO list.\n'
' - Explicitly ask: "Would you like me to add these tasks to your \'TODO-2026\' TODO list?"\n'
' - Do NOT modify any spreadsheet data until explicit approval is given.\n'
'4. Post-Approval Policy:\n'
f' - Upon receiving explicit user approval, first verify if the "TODO-2026" sheet tab exists in spreadsheet "{SPREADSHEET_ID}".\n'
f' - If the "TODO-2026" sheet tab does not exist, use the tool create_spreadsheet_tab to create it in spreadsheet "{SPREADSHEET_ID}".\n'
f' - Once the tab exists, use update_spreadsheet_values to append the approved adjustments as tasks to the "TODO-2026" sheet tab.\n'
' - Write the rows under the headers: Task (the actionable job, e.g., "Schedule a Support Barista role for Saturday morning"), Category ("Staffing" or "Inventory"), Ceremony, and Date_Added (today\'s date).\n'
' - Always confirm to the user exactly what tasks you have written to their "TODO-2026" TODO list.'
),
tools=[
FunctionTool(func=execute_sandbox_command),
FunctionTool(func=read_spreadsheet_values),
FunctionTool(func=update_spreadsheet_values),
FunctionTool(func=create_spreadsheet_tab)
]
)
adk_app = App(name="secure_sandbox_app", root_agent=root_agent)
runner = Runner(app=adk_app, session_service=InMemorySessionService(), auto_create_session=True)
app = FastAPI(title="Secure ADK Sandbox Assistant")
# ==========================================
# 4. ENDPOINTS & WEBSOCKET ROUTING
# ==========================================
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
active_connections.append(websocket)
await websocket.send_text("🔌 System: Connected. Agent is ready...")
try:
while True:
owner_reply = await websocket.receive_text()
print(f"Owner replied via WS: {owner_reply}")
await websocket.send_text("_Agent is running tools and thinking..._")
new_message = types.Content(parts=[types.Part(text=owner_reply)])
events = await asyncio.to_thread(
runner.run,
user_id="local_user",
session_id="local_session",
new_message=new_message
)
final_response = "".join(
part.text
for event in events
if event.content and event.content.parts
for part in event.content.parts
if part.text
) or "Agent completed execution updates without text output."
await websocket.send_text(final_response.strip())
except WebSocketDisconnect:
active_connections.remove(websocket)
class UserPrompt(BaseModel):
prompt: str
@app.post("/chat")
def chat_with_agent(payload: UserPrompt):
"""Fallback HTTP POST endpoint if UI is not used."""
try:
events = runner.run(
user_id="local_user",
session_id="local_session",
new_message=types.Content(parts=[types.Part(text=payload.prompt)])
)
final_response = "".join(
part.text
for event in events
if event.content and event.content.parts
for part in event.content.parts
if part.text
)
return {"status": "success", "response": final_response.strip()}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Agent loop failed: {str(e)}")
@app.get("/", response_class=HTMLResponse)
async def get_chat_ui():
"""Serves the warm coffee-themed chat interface."""
return """
<!DOCTYPE html>
<html>
<head>
<title>Coffee Shop Agent</title>
<!-- Load marked.js for client-side markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body { display: flex; height: 100vh; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
#sidebar { width: 250px; background: #3E2723; color: #EFEBE9; padding: 20px; }
#sidebar h2 { font-size: 1.3em; margin-top: 10px; color: #FFF; border-bottom: 1px solid #5D4037; padding-bottom: 10px; }
#main { flex-grow: 1; display: flex; flex-direction: column; background: #FAF8F6; }
#chat-history { flex-grow: 1; padding: 20px; overflow-y: auto; background: #F5EFEB; }
#input-area { padding: 20px; border-top: 1px solid #D7CCC8; background: #FAF8F6; display: flex;}
input {
flex-grow: 1;
padding: 12px;
border-radius: 6px;
border: 1px solid #D7CCC8;
margin-right: 10px;
font-size: 1em;
background: #FFF;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus {
outline: none;
border-color: #8D6E63;
box-shadow: 0 0 0 2px rgba(141, 110, 99, 0.25);
}
button {
padding: 10px 24px;
background: #6D4C41;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 1em;
transition: background-color 0.2s, transform 0.1s;
}
button:hover {
background: #5D4037;
}
button:active {
background: #4E342E;
transform: scale(0.98);
}
.message { margin-bottom: 15px; padding: 12px 16px; border-radius: 8px; max-width: 85%; line-height: 1.5; }
.user-msg { background: #EFEBE9; color: #3E2723; align-self: flex-end; margin-left: auto; border: 1px solid #D7CCC8;}
.agent-msg { background: #fff; color: #3E2723; border: 1px solid #E0DCD8; box-shadow: 0 1px 3px rgba(62,39,35,0.06); }
.msg-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.agent-name { font-weight: bold; color: #5D4037; margin: 0; font-size: 0.95em;}
.user-name { font-weight: bold; color: #8D6E63; margin: 0; font-size: 0.95em;}
.msg-timestamp { font-size: 0.8em; color: #8D6E63; font-weight: normal; }
.day-divider {
display: flex;
align-items: center;
text-align: center;
color: #8D6E63;
margin: 20px 0;
font-size: 0.85em;
font-weight: bold;
}
.day-divider::before, .day-divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #D7CCC8;
}
.day-divider:not(:empty)::before {
margin-right: .5em;
}
.day-divider:not(:empty)::after {
margin-left: .5em;
}
/* Markdown Styling inside Messages */
.message p { margin: 4px 0 8px 0; }
.message p:last-child { margin-bottom: 0; }
.message ul, .message ol { margin: 4px 0 8px 0; padding-left: 20px; }
.message li { margin-bottom: 3px; }
.message h1, .message h2, .message h3, .message h4 { margin: 12px 0 6px 0; font-size: 1.15em; color: #3E2723; font-weight: bold; }
.message h1:first-child, .message h2:first-child, .message h3:first-child { margin-top: 0; }
/* Table Styling */
.message table { border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 0.95em; }
.message th, .message td { border: 1px solid #D7CCC8; padding: 8px 10px; text-align: left; }
.message th { background-color: #F5EFEB; font-weight: bold; color: #3E2723; }
.message tr:nth-child(even) { background-color: #FAF8F6; }
/* Code / Blockquote styling */
.message code { background: #EFEBE9; padding: 2px 4px; border-radius: 3px; font-family: monospace; font-size: 0.9em; color: #5D4037; }
.message pre { background: #F5EFEB; padding: 10px; border-radius: 5px; overflow-x: auto; margin: 8px 0; border: 1px solid #D7CCC8; }
.message pre code { background: none; padding: 0; }
.message blockquote { margin: 8px 0; padding-left: 12px; border-left: 4px solid #6D4C41; color: #5D4037; }
</style>
</head>
<body>
<div id="sidebar">
<h2>☕ Coffee Shop Monitor</h2>
<p>Monitoring sheet...</p>
</div>
<div id="main">
<div id="chat-history"></div>
<div id="input-area">
<input type="text" id="msg" placeholder="Message Coffee Shop Monitor..." onkeypress="if(event.key === 'Enter') sendMessage()">
<button onclick="sendMessage()">Send</button>
</div>
</div>
<script>
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
const history = document.getElementById('chat-history');
// Scroll to the bottom on load to show latest messages
history.scrollTop = history.scrollHeight;
ws.onmessage = function(event) {
// Parse markdown content from the agent
const parsedHtml = marked.parse(event.data);
const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
history.innerHTML += `
<div class="message agent-msg">
<div class="msg-header">
<span class="agent-name">Inventory Agent APP</span>
<span class="msg-timestamp">${timeStr}</span>
</div>
<div>${parsedHtml}</div>
</div>`;
history.scrollTop = history.scrollHeight;
};
function sendMessage() {
const input = document.getElementById('msg');
const text = input.value;
if (!text) return;
// Parse user markdown if any (optional, but keeps styling consistent)
const parsedHtml = marked.parse(text);
const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
history.innerHTML += `
<div class="message user-msg">
<div class="msg-header">
<span class="user-name">You</span>
<span class="msg-timestamp">${timeStr}</span>
</div>
<div>${parsedHtml}</div>
</div>`;
ws.send(text);
input.value = '';
history.scrollTop = history.scrollHeight;
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
import uvicorn
port_val = int(os.environ.get('PORT', 8080))
uvicorn.run(app, host='0.0.0.0', port=port_val)
5. Triển khai dịch vụ Cloud Run
Thay vì tạo một hình ảnh, bạn sẽ sử dụng buildpack của Google Cloud để chuyển đổi mã nguồn ứng dụng của bạn thành một hình ảnh vùng chứa sẵn sàng cho hoạt động sản xuất trên Cloud Run.
gcloud beta run deploy coffee-mgr-agent \
--source=. \
--region=$REGION \
--sandbox-launcher \
--max-instances=1 \
--session-affinity \
--allow-unauthenticated \
--no-cpu-throttling \
--set-env-vars GOOGLE_GENAI_USE_VERTEXAI=1,GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=global,SPREADSHEET_ID=$SPREADSHEET_ID \
--service-account $SERVICE_ACCOUNT_ADDRESS
6. Trò chuyện với trợ lý
Sau khi triển khai, bạn sẽ thấy URL: https://coffee-mgr-agent-YOUR_PROJECT_ID.YOUR_REGION.run.app
Mở URL này trong trình duyệt.
Gửi lời nhắc sau đây cho trợ lý ảo.
The 2026 graduation schedule was just posted. It's the same schedule as last year. Can you review last year's POS data to help me prepare for this year?
Saturday, June 13:
College of Business (8:30 a.m.)
College of Science and Mathematics (12:30 p.m.)
College of Liberal Arts (4:30 p.m.)
Sunday, June 14:
College of Agriculture (8:30 a.m.)
College of Architecture (12:30 p.m.)
College of Engineering (4:30 p.m.)
Trợ lý ảo của bạn sẽ phản hồi bằng nội dung tương tự như các đề xuất sau:
Based on the 2025 POS data, I have correlated graduation ceremony patterns with beverage demand spikes. Last year, the post-ceremony windows consistently experienced high demand for complex items (Cold Brew, Extra Espresso, and Alt Milk). Key Data Discoveries for 2026 - College of Business (Saturday 8:30 a.m. ceremony): - Significant demand spike observed at 10:00 a.m. Saturday. - Data: 130 Cold Brews and 95 Extra Espresso shots. This is a primary bottleneck for barista output. - College of Engineering (Sunday 4:30 p.m. ceremony): - Significant demand spike observed at 6:00 p.m. Sunday. - Data: 145 Cold Brews and 110 Extra Espresso shots. This is the highest intensity period of the weekend. - Alt-Milk Surge (Saturday/Sunday 2:00 p.m. post-ceremonies): - Consistent, high-volume Alt Milk consumption (65-180 oz) after the 12:30 p.m. ceremonies. Diagnostic & Staffing Recommendations Given that these complex beverage items (Cold Brew, Extra Espresso) correlate directly with your high-traffic post-ceremony windows, the risk of hitting wait times > 10 minutes is high. Assuming you typically have 2 cashiers, my diagnostic is that these are barista output bottlenecks, not cashier bottlenecks. Suggested Tasks for your TODO-2026 List: 1. Staffing: Schedule a dedicated "Support Barista" for Saturday 9:30 a.m. – 11:00 a.m. to handle the Cold Brew/Espresso rush. 2. Staffing: Schedule a dedicated "Support Barista" for Sunday 5:30 p.m. – 7:00 p.m. to manage the high volume after the Engineering ceremony. 3. Inventory: Pre-stage extra Alt Milk cartons at the mobile station for Saturday 1:30 p.m. and Sunday 1:30 p.m. shifts. Would you like me to add these tasks to your 'TODO-2026' TODO list?
Sau đó, tác nhân sẽ yêu cầu bạn (Người tham gia vào quy trình) cấp quyền tạo/cập nhật trang tính TODO-2026.
Trả lời nhân viên hỗ trợ bằng Yes.
Bây giờ, khi xem trang tính TODO-2026, bạn sẽ thấy nội dung tương tự như sau:
Task Category Ceremony Date_Added Schedule a Support Barista role for Saturday morning rush Staffing College of Business 2025-05-22 Schedule a Support Barista role for Sunday evening rush Staffing College of Engineering 2025-05-22 Pre-stage extra Alt Milk cartons at mobile station Inventory College of Science/Math & Architecture 2025-05-22
7. Xin chúc mừng!
Chúc mừng bạn đã hoàn thành lớp học lập trình này!
Bạn nên xem tài liệu về Cloud Run, tài liệu về hộp cát Cloud Run và tài liệu về ADK.
Nội dung đã đề cập
- Cách tạo một dịch vụ Cloud Run
- Cách triển khai một tác nhân ADK trên dịch vụ Cloud Run
- Cách để một tác nhân chạy mã trong hộp cát trong một dịch vụ Cloud Run
8. Dọn dẹp
Để không bị tính phí cho tài khoản Google Cloud đối với các tài nguyên được dùng trong hướng dẫn này, bạn có thể xoá dự án hoặc xoá từng tài nguyên.
Cách 1: Xoá tài nguyên
Sau đây là thông tin tổng quan ngắn gọn về các bước để dọn dẹp dự án và tránh các khoản phí không cần thiết.
Xoá dịch vụ Cloud Run
gcloud run services delete coffee-mgr-agent --region $REGION
Xoá tài khoản dịch vụ: Xoá tài khoản dịch vụ bảng tính chuyên dụng
gcloud iam service-accounts delete $SERVICE_ACCOUNT_ADDRESS
Cách 2: Xoá dự án
Để xoá toàn bộ dự án, hãy chuyển đến phần Quản lý tài nguyên, chọn dự án bạn đã tạo ở Bước 2 rồi chọn Xoá. Nếu xoá dự án, bạn sẽ cần thay đổi dự án trong Cloud SDK. Bạn có thể xem danh sách tất cả các dự án có sẵn bằng cách chạy gcloud projects list. Nếu muốn sử dụng dòng lệnh, bạn cũng có thể dùng lệnh sau:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}