Cách triển khai ứng dụng chatbot FastAPI lên Cloud Run bằng Gemini

1. Giới thiệu

Tổng quan

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách triển khai ứng dụng FastAPI lên Cloud Run. Ứng dụng này là một ứng dụng chatbot nhắc một mô hình Gemini.

Kiến thức bạn sẽ học được

  • Cách triển khai FastAPI cho Cloud Run
  • Gợi ý Gemini từ Cloud Run trong python bằng cách sử dụng thư viện ứng dụng Google

2. Cách thiết lập và các yêu cầu

Thiết lập các biến môi trường sẽ được sử dụng trong suốt lớp học lập trình này.

PROJECT_ID=<YOUR_PROJECT_ID>
REGION=<YOUR_REGION>
GEMINI_MODEL=gemini-2.0-flash-001

SERVICE_NAME=fastapi-gemini
SERVICE_ACCOUNT=fastapi-gemini-sa
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

Tạo tài khoản dịch vụ bằng cách chạy lệnh sau:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Service Account for FastAPI Gemini CR service"

Cấp cho tài khoản dịch vụ của bạn quyền truy cập vào Gemini với vai trò Người dùng Vertex AI.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_ADDRESS" \
  --role="roles/aiplatform.user"

3. Tạo ứng dụng

Tạo thư mục cho mã của bạn.

mkdir codelab-cr-fastapi-gemini
cd codelab-cr-fastapi-gemini

Trước tiên, bạn sẽ tạo các mẫu html bằng cách tạo thư mục mẫu.

mkdir templates
cd templates

Tạo một tệp mới có tên ai_message.html với nội dung sau:

<div class="message-container ai-message-container">
    {{ ai_response_text }}
</div>

Tạo một tệp mới có tên message.html với nội dung sau:

<div class="message-container user-message">
    {{ message }}
</div>

Tạo một tệp mới có tên index.html với nội dung sau:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FastAPI HTMX Gemini Chat</title>
    <style>
        body { font-family: sans-serif; max-width: 700px; margin: auto; padding: 20px; background-color: #f4f4f4; }
        #chat-messages { border: 1px solid #ccc; background-color: #fff; padding: 15px; height: 400px; overflow-y: scroll; margin-bottom: 15px; border-radius: 5px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); }
        .message-container { margin-bottom: 10px; padding: 8px 12px; border-radius: 15px; max-width: 80%; word-wrap: break-word; }
        .user-message { background-color: #dcf8c6; align-self: flex-end; margin-left: auto; text-align: right; border-bottom-right-radius: 0;}
        .ai-message-container { background-color: #eee; align-self: flex-start; margin-right: auto; border-bottom-left-radius: 0;}
        .ai-message-container p { margin: 0.2em 0; } /* Spacing for streamed paragraphs */
        .ai-message-container p:first-child { margin-top: 0; }
        .ai-message-container p:last-child { margin-bottom: 0; }
        form { display: flex; margin-top: 10px; }
        input[type="text"] { flex-grow: 1; padding: 10px; border: 1px solid #ccc; border-radius: 20px; margin-right: 10px; }
        button { padding: 10px 20px; background-color: #0b93f6; color: white; border: none; border-radius: 20px; cursor: pointer; font-weight: bold; }
        button:hover { background-color: #0a84dd; }
    </style>
    <script src="https://unpkg.com/htmx.org@2.0.4"
    integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/htmx-ext-sse@2.2.2" crossorigin="anonymous"></script>
</head>
<body>

    <h1>Chat with Gemini</h1>

    <div id="chat-messages">
        {% for msg in messages %}
             {# Render initial messages if needed #}
        {% endfor %}
    </div>

    <form
        hx-post="/ask"             {# Post to the /ask endpoint #}
        hx-target="#chat-messages" {# Target the main chat area #}
        hx-swap="beforeend"        {# Append the response (user msg + AI placeholder) #}
        hx-on::after-request="this.reset(); document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;" {# Clear form & scroll down #}
        >
        <input type="text" name="message" placeholder="Ask Gemini..." autofocus autocomplete="off">
        <button type="submit">Send</button>
    </form>

    <script>
        // Initial scroll to bottom on page load (if needed)
        window.onload = () => {
            const chatBox = document.getElementById('chat-messages');
            chatBox.scrollTop = chatBox.scrollHeight;
        }
    </script>

</body>
</html>

Bây giờ, hãy tạo mã python và các tệp khác trong thư mục gốc

cd ..

Tạo tệp .gcloudignore có nội dung sau:

__pycache__

Tạo một tệp có tên main.py với nội dung sau:

from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from typing import List, Annotated
from google import genai
import os

# in case the env var isn't set, use YOUR_<VARIABLE> as the default
# to help with debugging
project_id = os.getenv("PROJECT_ID", "YOUR_PROJECT_ID")
region = os.getenv("REGION", "YOUR_REGION")
gemini_model = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-001")

app = FastAPI(title="FastAPI HTMX Chat")

templates = Jinja2Templates(directory="templates")

genai_client = genai.Client(
    vertexai=True, project=project_id, location=region
)

system_prompt = f"""
You're a chatbot that helps pass the time with small talk, that is
polite conversation about unimportant or uncontroversial matters
that allows people to pass the time. Please keep your answers short.
"""

chat_messages: List[str] = []

# --- Routes ---
@app.get("/", response_class=HTMLResponse)
async def get_chat_ui(request: Request):
    """Serves the main chat page."""
    print("Serving index.html")
    return templates.TemplateResponse(
        "index.html",
        {"request": request, "messages": chat_messages} # Pass existing messages
    )

@app.post("/ask", response_class=HTMLResponse)
async def ask_gemini_and_respond(
    request: Request,
    # Use Annotated for dependency injection with Form data
    message: Annotated[str, Form()]
):
    
    user_msg_html = templates.get_template('message.html').render({'message': message})
    
    print("asking gemini...")
    response = genai_client.models.generate_content(
        model=gemini_model,
        contents=[message],
        config=genai.types.GenerateContentConfig(
            system_instruction=system_prompt,
            temperature=0.7,
        ),
    )
    
    print("Gemini responded with: " + response.text)
    
    ai_response_html = templates.get_template('ai_message.html').render({'ai_response_text': response.text})

    combined_html = user_msg_html + ai_response_html

    return HTMLResponse(content=combined_html)

Tạo Dockerfile có nội dung sau:

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app

# Install poetry
RUN pip install poetry
RUN poetry self add poetry-plugin-export

# Copy poetry files
COPY pyproject.toml poetry.lock* ./

# Copy application code
COPY . .

# Export dependencies to requirements.txt
RUN poetry export -f requirements.txt --output requirements.txt 

# Final stage
FROM python:3.12-slim

RUN apt-get update && apt-get install -y libcairo2 python3-dev libffi-dev

WORKDIR /app

# Copy files from builder
COPY --from=builder /app/ .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Compile bytecode to improve startup latency
# -q: Quiet mode 
# -b: Write legacy bytecode files (.pyc) alongside source
# -f: Force rebuild even if timestamps are up-to-date
RUN python -m compileall -q -b -f .

# Expose port
EXPOSE 8080

# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Tạo tệp pyproject.toml

[tool.poetry]
name = "codelab"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.115.12"
uvicorn = {extras = ["standard"], version = "^0.34.0"}
jinja2 = "^3.1.6"
python-multipart = "^0.0.20"
google-genai = "^1.8.0"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

4. Triển khai lên Cloud Run

gcloud run deploy $SERVICE_NAME \
 --source . \
 --allow-unauthenticated \
 --service-account=$SERVICE_ACCOUNT_ADDRESS \
 --set-env-vars=PROJECT_ID=$PROJECT_ID \
 --set-env-vars=REGION=$REGION \
 --set-env-vars=GEMINI_MODEL=$GEMINI_MODEL

5. Thử nghiệm dịch vụ

Mở URL của Dịch vụ trong trình duyệt web và đặt câu hỏi cho Gemini, ví dụ: Tại sao bầu trời có màu xanh dương?

6. Xin chúc mừng!

Chúc mừng bạn đã hoàn thành lớp học lập trình!

Nội dung đã đề cập

  • Cách triển khai FastAPI cho Cloud Run
  • Gợi ý Gemini từ Cloud Run trong python bằng cách sử dụng thư viện ứng dụng Google

7. Dọn dẹp

Để xoá dịch vụ Cloud Run, hãy truy cập vào Cloud Console của Cloud Run tại https://console.cloud.google.com/run rồi xoá dịch vụ đó.

Nếu chọn xoá toàn bộ dự án, bạn có thể truy cập vào https://console.cloud.google.com/cloud-resource-manager, 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 SDK trên đám mây. Bạn có thể xem danh sách tất cả dự án hiện có bằng cách chạy gcloud projects list.