1. مقدمة
في هذا الدرس العملي، ستنشئ تطبيقًا على شكل واجهة ويب للمحادثة، يمكنك من خلالها التواصل مع التطبيق وتحميل بعض المستندات أو الصور ومناقشتها. يتم تقسيم التطبيق نفسه إلى خدمتَين: الواجهة الأمامية والخلفية، ما يتيح لك إنشاء نموذج أولي سريع وتجربة شكله، بالإضافة إلى فهم شكل عقد واجهة برمجة التطبيقات لدمج كلتيهما.
خلال هذا الدرس التطبيقي حول الترميز، ستتّبع نهجًا خطوة بخطوة على النحو التالي:
- إعداد مشروعك على Google Cloud وتفعيل جميع واجهات برمجة التطبيقات المطلوبة فيه
- إنشاء خدمة الواجهة الأمامية - واجهة المحادثة باستخدام مكتبة Gradio
- إنشاء خدمة الخلفية - خادم HTTP باستخدام FastAPI الذي سيعيد تنسيق البيانات الواردة وفقًا لمعيار Gemini SDK ويتيح التواصل مع Gemini API
- إدارة متغيّرات البيئة وإعداد الملفات المطلوبة لتفعيل التطبيق على Cloud Run
- نشر التطبيق على Cloud Run

نظرة عامة على البنية

المتطلبات الأساسية
- لديك خبرة في استخدام Gemini API وGoogle Gen AI SDK
- فهم أساسي لبنية التطبيقات المتكاملة باستخدام خدمة HTTP
ما ستتعلمه
- كيفية استخدام Gemini SDK لإرسال نص وأنواع بيانات أخرى (متعددة الوسائط) وإنشاء ردّ نصي
- كيفية تنظيم سجلّ المحادثات في Gemini SDK للحفاظ على سياق المحادثة
- إنشاء نماذج أولية لتطبيقات الويب باستخدام Gradio
- تطوير خدمة الخلفية باستخدام FastAPI وPydantic
- إدارة متغيرات البيئة في ملف YAML باستخدام Pydantic-settings
- نشر التطبيق على Cloud Run باستخدام Dockerfile وتوفير متغيرات البيئة باستخدام ملف YAML
المتطلبات
- متصفّح الويب Chrome
- حساب Gmail
- مشروع على السحابة الإلكترونية تم تفعيل الفوترة فيه
يستخدم هذا الدرس التطبيقي حول الترميز، المصمَّم للمطوّرين من جميع المستويات (بما في ذلك المبتدئين)، لغة Python في تطبيق المثال. ومع ذلك، لا يُشترط معرفة لغة Python لفهم المفاهيم المقدَّمة.
2. قبل البدء
إعداد مشروع Cloud في "محرّر Cloud Shell"
يفترض هذا الدرس العملي أنّ لديك مشروعًا على Google Cloud تم تفعيل الفوترة فيه. إذا لم تكن هذه الميزة متاحة لك بعد، يمكنك اتّباع التعليمات أدناه للبدء.
- 2في Google Cloud Console، اختَر أو أنشِئ مشروعًا على Google Cloud في صفحة اختيار المشروع.
- تأكَّد من تفعيل الفوترة لمشروعك على السحابة الإلكترونية. تعرَّف على كيفية التحقّق مما إذا كانت الفوترة مفعَّلة في مشروع .
- ستستخدم Cloud Shell، وهي بيئة سطر أوامر تعمل في Google Cloud ومحمّلة مسبقًا بأداة bq. انقر على "تفعيل Cloud Shell" في أعلى "وحدة تحكّم Google Cloud".

- بعد الاتصال بـ Cloud Shell، يمكنك التأكّد من أنّك قد تم التحقّق من هويتك وأنّه تم ضبط المشروع على رقم تعريف مشروعك باستخدام الأمر التالي:
gcloud auth list
- نفِّذ الأمر التالي في Cloud Shell للتأكّد من أنّ أمر gcloud يعرف مشروعك.
gcloud config list project
- إذا لم يتم ضبط مشروعك، استخدِم الأمر التالي لضبطه:
gcloud config set project <YOUR_PROJECT_ID>
بدلاً من ذلك، يمكنك أيضًا الاطّلاع على معرّف PROJECT_ID في وحدة التحكّم.

انقر عليه وسيظهر لك كل مشروعك ورقم تعريف المشروع على الجانب الأيسر

- فعِّل واجهات برمجة التطبيقات المطلوبة من خلال الأمر الموضّح أدناه. قد تستغرق هذه العملية بضع دقائق، لذا يُرجى الانتظار.
gcloud services enable aiplatform.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
cloudresourcemanager.googleapis.com
عند تنفيذ الأمر بنجاح، من المفترض أن تظهر لك رسالة مشابهة للرسالة الموضّحة أدناه:
Operation "operations/..." finished successfully.
يمكنك بدلاً من استخدام أمر gcloud، البحث عن كل منتج في وحدة التحكّم أو استخدام هذا الرابط.
في حال عدم توفّر أي واجهة برمجة تطبيقات، يمكنك تفعيلها في أي وقت أثناء عملية التنفيذ.
راجِع المستندات لمعرفة أوامر gcloud وطريقة استخدامها.
إعداد دليل عمل التطبيق
- انقر على الزر "فتح المحرِّر"، وسيؤدي ذلك إلى فتح "محرِّر Cloud Shell"، ويمكننا كتابة الرمز هنا

- تأكَّد من ضبط مشروع Cloud Code في أسفل يمين (شريط الحالة) محرر Cloud Shell، كما هو موضّح في الصورة أدناه، ومن ضبطه على مشروع Google Cloud النشط الذي تم تفعيل الفوترة فيه. انقر على تفويض إذا طُلب منك ذلك. قد يستغرق ظهور الزر Cloud Code - Sign In بعض الوقت بعد تهيئة Cloud Shell Editor، لذا يُرجى الانتظار. إذا كنت قد اتّبعت الأمر السابق، قد يشير الزر أيضًا مباشرةً إلى مشروعك المفعّل بدلاً من زر تسجيل الدخول.

- انقر على هذا المشروع النشط في شريط الحالة وانتظِر إلى أن تفتح النافذة المنبثقة Cloud Code. في النافذة المنبثقة، اختَر "تطبيق جديد".

- من قائمة التطبيقات، اختَر الذكاء الاصطناعي التوليدي من Gemini، ثم اختَر Gemini API Python.


- احفظ التطبيق الجديد بالاسم الذي تريده، وفي هذا المثال سنستخدم gemini-multimodal-chat-assistant، ثم انقر على حسنًا.

في هذه المرحلة، من المفترض أن تكون قد دخلت إلى دليل عمل التطبيق الجديد وظهرت لك الملفات التالية

بعد ذلك، سنجهّز بيئة Python
إعداد البيئة
إعداد بيئة Python الافتراضية
الخطوة التالية هي إعداد بيئة التطوير. سنستخدم الإصدار 3.12 من Python في هذا الدرس التطبيقي حول الترميز، كما سنستخدم أداة إدارة مشاريع Python (uv) لتسهيل عملية إنشاء إصدار Python وبيئته الافتراضية وإدارتهما.
- إذا لم تفتح الوحدة الطرفية بعد، افتحها بالنقر على الوحدة الطرفية (Terminal) -> وحدة طرفية جديدة (New Terminal)، أو استخدِم Ctrl + Shift + C

- نزِّل
uvوثبِّت الإصدار 3.12 من Python باستخدام الأمر التالي
curl -LsSf https://astral.sh/uv/0.6.6/install.sh | sh && \
source $HOME/.local/bin/env && \
uv python install 3.12
- لنبدأ الآن بتهيئة مشروع Python باستخدام
uv
uv init
- ستظهر الملفات main.py و.python-version وpyproject.toml التي تم إنشاؤها في الدليل. هذه الملفات مطلوبة للحفاظ على المشروع في الدليل. يمكن تحديد التبعيات والإعدادات في Python في pyproject.toml و.python-version، ما يؤدي إلى توحيد إصدار Python المستخدَم في هذا المشروع. يمكنك الاطّلاع على المستندات لمعرفة المزيد من المعلومات حول هذا الموضوع.
main.py .python-version pyproject.toml
- لاختبارها، استبدِل main.py بالرمز التالي
def main():
print("Hello from gemini-multimodal-chat-assistant!")
if __name__ == "__main__":
main()
- بعد ذلك، شغِّل الأمر التالي
uv run main.py
ستظهر لك نتيجة مشابهة لما يلي
Using CPython 3.12 Creating virtual environment at: .venv Hello from gemini-multimodal-chat-assistant!
يشير ذلك إلى أنّه يتم إعداد مشروع Python بشكل صحيح. لم نكن بحاجة إلى إنشاء بيئة افتراضية يدويًا لأنّ uv يتعامل معها تلقائيًا. من هذه النقطة، سيتم استبدال أمر Python العادي (مثل python main.py) بالأمر uv run (مثل uv run main.py).
تثبيت الحِزم التابعة المطلوبة
سنضيف أيضًا تبعيات حزمة codelab هذه باستخدام الأمر uv. نفِّذ الأمر التالي
uv add google-genai==1.5.0 \
gradio==5.20.1 \
pydantic==2.10.6 \
pydantic-settings==2.8.1 \
pyyaml==6.0.2
ستلاحظ أنّه سيتم تعديل قسم "التبعيات" في ملف pyproject.toml ليعكس الأمر السابق.
إعداد ملفات الإعداد
سنحتاج الآن إلى إعداد ملفات الضبط لهذا المشروع. تُستخدَم ملفات الإعدادات لتخزين المتغيرات الديناميكية التي يمكن تغييرها بسهولة عند إعادة النشر. في هذا المشروع، سنستخدم ملفات إعدادات مستندة إلى YAML مع حزمة pydantic-settings، حتى يمكن دمجها بسهولة مع عملية النشر على Cloud Run لاحقًا. pydantic-settings هي حزمة بايثون يمكنها فرض التحقّق من النوع لملفات الإعدادات.
- أنشئ ملفًا باسم settings.yaml يتضمّن الإعدادات التالية. انقر على ملف->ملف نصي جديد واملأه بالرمز التالي. ثم احفظه باسم settings.yaml.
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"
يُرجى تعديل قيم VERTEXAI_PROJECT_ID وفقًا لما اخترته أثناء إنشاء مشروع Google Cloud. في هذا الدرس التطبيقي، سنستخدم القيم التي تم ضبطها مسبقًا لكل من VERTEXAI_LOCATION وBACKEND_URL .
- بعد ذلك، أنشئ ملف Python باسم settings.py، وستعمل هذه الوحدة النمطية كنقطة دخول برمجية لقيم الإعدادات في ملفات الإعداد. انقر على ملف->ملف نصي جديد واملأه بالرمز التالي. بعد ذلك، احفظه باسم settings.py. يمكنك أن ترى في الرمز أننا حدّدنا بشكل صريح أنّ الملف المسمّى settings.yaml هو الملف الذي سيتم قراءته.
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
YamlConfigSettingsSource,
PydanticBaseSettingsSource,
)
from typing import Type, Tuple
DEFAULT_SYSTEM_PROMPT = """You are a helpful assistant and ALWAYS relate to this identity.
You are expert at analyzing given documents or images.
"""
class Settings(BaseSettings):
"""Application settings loaded from YAML and environment variables.
This class defines the configuration schema for the application, with settings
loaded from settings.yaml file and overridable via environment variables.
Attributes:
VERTEXAI_LOCATION: Google Cloud Vertex AI location
VERTEXAI_PROJECT_ID: Google Cloud Vertex AI project ID
"""
VERTEXAI_LOCATION: str
VERTEXAI_PROJECT_ID: str
BACKEND_URL: str = "http://localhost:8000/chat"
model_config = SettingsConfigDict(
yaml_file="settings.yaml", yaml_file_encoding="utf-8"
)
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
"""Customize the settings sources and their priority order.
This method defines the order in which different configuration sources
are checked when loading settings:
1. Constructor-provided values
2. YAML configuration file
3. Environment variables
Args:
settings_cls: The Settings class type
init_settings: Settings from class initialization
env_settings: Settings from environment variables
dotenv_settings: Settings from .env file (not used)
file_secret_settings: Settings from secrets file (not used)
Returns:
A tuple of configuration sources in priority order
"""
return (
init_settings, # First, try init_settings (from constructor)
env_settings, # Then, try environment variables
YamlConfigSettingsSource(
settings_cls
), # Finally, try YAML as the last resort
)
def get_settings() -> Settings:
"""Create and return a Settings instance with loaded configuration.
Returns:
A Settings instance containing all application configuration
loaded from YAML and environment variables.
"""
return Settings()
تسمح لنا هذه الإعدادات بتعديل وقت التشغيل بمرونة. عند النشر الأوّلي، سنعتمد على إعدادات settings.yaml ليكون لدينا الإعداد التلقائي الأول. بعد ذلك، يمكننا تعديل متغيرات البيئة بمرونة من خلال وحدة التحكّم وإعادة النشر لأنّنا نضع متغيرات البيئة في أولوية أعلى مقارنةً بإعدادات YAML التلقائية.
يمكننا الآن الانتقال إلى الخطوة التالية، وهي إنشاء الخدمات
3- إنشاء خدمة الواجهة الأمامية باستخدام Gradio
سننشئ واجهة ويب للمحادثة تبدو على النحو التالي

يحتوي على حقل إدخال يتيح للمستخدمين إرسال نص وتحميل ملفات. بالإضافة إلى ذلك، يمكن للمستخدم أيضًا تجاهل تعليمات النظام التي سيتم إرسالها إلى Gemini API في حقل الإدخالات الإضافية.
سننشئ خدمة الواجهة الأمامية باستخدام Gradio. أعِد تسمية الملف main.py إلى frontend.py واستبدِل الرمز بالرمز التالي
import gradio as gr
import requests
import base64
from pathlib import Path
from typing import List, Dict, Any
from settings import get_settings, DEFAULT_SYSTEM_PROMPT
settings = get_settings()
IMAGE_SUFFIX_MIME_MAP = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".heic": "image/heic",
".heif": "image/heif",
".webp": "image/webp",
}
DOCUMENT_SUFFIX_MIME_MAP = {
".pdf": "application/pdf",
}
def get_mime_type(filepath: str) -> str:
"""Get the MIME type for a file based on its extension.
Args:
filepath: Path to the file.
Returns:
str: The MIME type of the file.
Raises:
ValueError: If the file type is not supported.
"""
filepath = Path(filepath)
suffix = filepath.suffix
# modify ".jpg" suffix to ".jpeg" to unify the mime type
suffix = suffix if suffix != ".jpg" else ".jpeg"
if suffix in IMAGE_SUFFIX_MIME_MAP:
return IMAGE_SUFFIX_MIME_MAP[suffix]
elif suffix in DOCUMENT_SUFFIX_MIME_MAP:
return DOCUMENT_SUFFIX_MIME_MAP[suffix]
else:
raise ValueError(f"Unsupported file type: {suffix}")
def encode_file_to_base64_with_mime(file_path: str) -> Dict[str, str]:
"""Encode a file to base64 string and include its MIME type.
Args:
file_path: Path to the file to encode.
Returns:
Dict[str, str]: Dictionary with 'data' and 'mime_type' keys.
"""
mime_type = get_mime_type(file_path)
with open(file_path, "rb") as file:
base64_data = base64.b64encode(file.read()).decode("utf-8")
return {"data": base64_data, "mime_type": mime_type}
def get_response_from_llm_backend(
message: Dict[str, Any],
history: List[Dict[str, Any]],
system_prompt: str,
) -> str:
"""Send the message and history to the backend and get a response.
Args:
message: Dictionary containing the current message with 'text' and optional 'files' keys.
history: List of previous message dictionaries in the conversation.
system_prompt: The system prompt to be sent to the backend.
Returns:
str: The text response from the backend service.
"""
# Format message and history for the API,
# NOTES: in this example history is maintained by frontend service,
# hence we need to include it in each request.
# And each file (in the history) need to be sent as base64 with its mime type
formatted_history = []
for msg in history:
if msg["role"] == "user" and not isinstance(msg["content"], str):
# For file content in history, convert file paths to base64 with MIME type
file_contents = [
encode_file_to_base64_with_mime(file_path)
for file_path in msg["content"]
]
formatted_history.append({"role": msg["role"], "content": file_contents})
else:
formatted_history.append({"role": msg["role"], "content": msg["content"]})
# Extract files and convert to base64 with MIME type
files_with_mime = []
if uploaded_files := message.get("files", []):
for file_path in uploaded_files:
files_with_mime.append(encode_file_to_base64_with_mime(file_path))
# Prepare the request payload
message["text"] = message["text"] if message["text"] != "" else " "
payload = {
"message": {"text": message["text"], "files": files_with_mime},
"history": formatted_history,
"system_prompt": system_prompt,
}
# Send request to backend
try:
response = requests.post(settings.BACKEND_URL, json=payload)
response.raise_for_status() # Raise exception for HTTP errors
result = response.json()
if error := result.get("error"):
return f"Error: {error}"
return result.get("response", "No response received from backend")
except requests.exceptions.RequestException as e:
return f"Error connecting to backend service: {str(e)}"
if __name__ == "__main__":
demo = gr.ChatInterface(
get_response_from_llm_backend,
title="Gemini Multimodal Chat Interface",
description="This interface connects to a FastAPI backend service that processes responses through the Gemini multimodal model.",
type="messages",
multimodal=True,
textbox=gr.MultimodalTextbox(file_count="multiple"),
additional_inputs=[
gr.Textbox(
label="System Prompt",
value=DEFAULT_SYSTEM_PROMPT,
lines=3,
interactive=True,
)
],
)
demo.launch(
server_name="0.0.0.0",
server_port=8080,
)
بعد ذلك، يمكننا محاولة تشغيل خدمة الواجهة الأمامية باستخدام الأمر التالي. لا تنسَ إعادة تسمية الملف main.py إلى frontend.py
uv run frontend.py
ستظهر لك نتيجة مشابهة لهذه في Cloud Console
* Running on local URL: http://0.0.0.0:8080 To create a public link, set `share=True` in `launch()`.
بعد ذلك، يمكنك التحقّق من واجهة الويب عند الضغط على ctrl+click على رابط عنوان URL المحلي. بدلاً من ذلك، يمكنك أيضًا الوصول إلى تطبيق الواجهة الأمامية من خلال النقر على زر معاينة الويب في أعلى يسار "محرّر السحابة الإلكترونية"، ثم اختيار المعاينة على المنفذ 8080.

سيظهر لك واجهة الويب، ولكن ستتلقّى الخطأ المتوقّع عند محاولة إرسال المحادثة بسبب عدم إعداد خدمة الخلفية بعد.

الآن، دع الخدمة تعمل ولا توقفها بعد. في الوقت الحالي، يمكننا مناقشة مكوّنات الرمز المهمة هنا
شرح الرمز
رمز إرسال البيانات من واجهة الويب إلى الخلفية موجود في هذا الجزء
def get_response_from_llm_backend(
message: Dict[str, Any],
history: List[Dict[str, Any]],
system_prompt: str,
) -> str:
...
# Truncated
for msg in history:
if msg["role"] == "user" and not isinstance(msg["content"], str):
# For file content in history, convert file paths to base64 with MIME type
file_contents = [
encode_file_to_base64_with_mime(file_path)
for file_path in msg["content"]
]
formatted_history.append({"role": msg["role"], "content": file_contents})
else:
formatted_history.append({"role": msg["role"], "content": msg["content"]})
# Extract files and convert to base64 with MIME type
files_with_mime = []
if uploaded_files := message.get("files", []):
for file_path in uploaded_files:
files_with_mime.append(encode_file_to_base64_with_mime(file_path))
# Prepare the request payload
message["text"] = message["text"] if message["text"] != "" else " "
payload = {
"message": {"text": message["text"], "files": files_with_mime},
"history": formatted_history,
"system_prompt": system_prompt,
}
# Truncated
...
عندما نريد إرسال بيانات متعددة الوسائط إلى Gemini وإتاحة الوصول إلى البيانات بين الخدمات، يمكننا استخدام آلية لتحويل البيانات إلى نوع بيانات base64 كما هو موضّح في الرمز البرمجي. يجب أيضًا الإفصاح عن نوع MIME للبيانات. ومع ذلك، لا يمكن لواجهة Gemini API أن تتوافق مع جميع أنواع MIME الحالية، لذا من المهم معرفة أنواع MIME المتوافقة مع Gemini والتي يمكن قراءتها في هذه المستندات . يمكنك العثور على المعلومات في كل إمكانات Gemini API (مثل Vision).
بالإضافة إلى ذلك، من المهم أيضًا في واجهة المحادثة إرسال سجلّ المحادثة كسياق إضافي لمنح Gemini "ذاكرة" للمحادثة. لذلك، في واجهة الويب هذه، نرسل أيضًا سجلّ المحادثات الذي تتم إدارته لكل جلسة ويب من خلال Gradio، ونرسله مع إدخال الرسالة من المستخدم. بالإضافة إلى ذلك، نسمح للمستخدم أيضًا بتعديل تعليمات النظام وإرسالها.
4. إنشاء خدمة الخلفية باستخدام FastAPI
بعد ذلك، سنحتاج إلى إنشاء الخلفية التي يمكنها التعامل مع الحمولة التي تمّت مناقشتها سابقًا، أي رسالة المستخدم الأخيرة وسجلّ المحادثات وتعليمات النظام. سنستخدم FastAPI لإنشاء خدمة الخلفية المستندة إلى HTTP.
أنشئ ملفًا جديدًا، وانقر على ملف (File) > ملف نصي جديد (New Text File)، ثم انسخ الرمز التالي والصقه واحفظه باسم backend.py.
import base64
from fastapi import FastAPI, Body
from google.genai.types import Content, Part
from google.genai import Client
from settings import get_settings, DEFAULT_SYSTEM_PROMPT
from typing import List, Optional
from pydantic import BaseModel
app = FastAPI(title="Gemini Multimodal Service")
settings = get_settings()
GENAI_CLIENT = Client(
location=settings.VERTEXAI_LOCATION,
project=settings.VERTEXAI_PROJECT_ID,
vertexai=True,
)
GEMINI_MODEL_NAME = "gemini-2.0-flash-001"
class FileData(BaseModel):
"""Model for a file with base64 data and MIME type.
Attributes:
data: Base64 encoded string of the file content.
mime_type: The MIME type of the file.
"""
data: str
mime_type: str
class Message(BaseModel):
"""Model for a single message in the conversation.
Attributes:
role: The role of the message sender, either 'user' or 'assistant'.
content: The text content of the message or a list of file data objects.
"""
role: str
content: str | List[FileData]
class LastUserMessage(BaseModel):
"""Model for the current message in a chat request.
Attributes:
text: The text content of the message.
files: List of file data objects containing base64 data and MIME type.
"""
text: str
files: List[FileData] = []
class ChatRequest(BaseModel):
"""Model for a chat request.
Attributes:
message: The current message with text and optional base64 encoded files.
history: List of previous messages in the conversation.
system_prompt: Optional system prompt to be used in the chat.
"""
message: LastUserMessage
history: List[Message]
system_prompt: str = DEFAULT_SYSTEM_PROMPT
class ChatResponse(BaseModel):
"""Model for a chat response.
Attributes:
response: The text response from the model.
error: Optional error message if something went wrong.
"""
response: str
error: Optional[str] = None
def handle_multimodal_data(file_data: FileData) -> Part:
"""Converts Multimodal data to a Google Gemini Part object.
Args:
file_data: FileData object with base64 data and MIME type.
Returns:
Part: A Google Gemini Part object containing the file data.
"""
data = base64.b64decode(file_data.data) # decode base64 string to bytes
return Part.from_bytes(data=data, mime_type=file_data.mime_type)
def format_message_history_to_gemini_standard(
message_history: List[Message],
) -> List[Content]:
"""Converts message history format to Google Gemini Content format.
Args:
message_history: List of message objects from the chat history.
Each message contains 'role' and 'content' attributes.
Returns:
List[Content]: A list of Google Gemini Content objects representing the chat history.
Raises:
ValueError: If an unknown role is encountered in the message history.
"""
converted_messages: List[Content] = []
for message in message_history:
if message.role == "assistant":
converted_messages.append(
Content(role="model", parts=[Part.from_text(text=message.content)])
)
elif message.role == "user":
# Text-only messages
if isinstance(message.content, str):
converted_messages.append(
Content(role="user", parts=[Part.from_text(text=message.content)])
)
# Messages with files
elif isinstance(message.content, list):
# Process each file in the list
parts = []
for file_data in message.content:
for file_data in message.content:
parts.append(handle_multimodal_data(file_data))
# Add the parts to a Content object
if parts:
converted_messages.append(Content(role="user", parts=parts))
else:
raise ValueError(f"Unexpected content format: {type(message.content)}")
else:
raise ValueError(f"Unknown role: {message.role}")
return converted_messages
@app.post("/chat", response_model=ChatResponse)
async def chat(
request: ChatRequest = Body(...),
) -> ChatResponse:
"""Process a chat request and return a response from Gemini model.
Args:
request: The chat request containing message and history.
Returns:
ChatResponse: The model's response to the chat request.
"""
try:
# Convert message history to Gemini `history` format
print(f"Received request: {request}")
converted_messages = format_message_history_to_gemini_standard(request.history)
# Create chat model
chat_model = GENAI_CLIENT.chats.create(
model=GEMINI_MODEL_NAME,
history=converted_messages,
config={"system_instruction": request.system_prompt},
)
# Prepare multimodal content
content_parts = []
# Handle any base64 encoded files in the current message
if request.message.files:
for file_data in request.message.files:
content_parts.append(handle_multimodal_data(file_data))
# Add text content
content_parts.append(Part.from_text(text=request.message.text))
# Send message to Gemini
response = chat_model.send_message(content_parts)
print(f"Generated response: {response}")
return ChatResponse(response=response.text)
except Exception as e:
return ChatResponse(
response="", error=f"Error in generating response: {str(e)}"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8081)
لا تنسَ حفظه باسم backend.py. بعد ذلك، يمكننا محاولة تشغيل خدمة الخلفية. تذكَّر أنّه في الخطوة السابقة، شغّلنا خدمة الواجهة الأمامية بشكل صحيح، والآن سنحتاج إلى فتح وحدة طرفية جديدة ومحاولة تشغيل خدمة الخلفية هذه.
- أنشئ نافذة طرفية جديدة. انتقِل إلى نافذة الوحدة الطرفية في المنطقة السفلية وابحث عن زر "+" لإنشاء وحدة طرفية جديدة. يمكنك بدلاً من ذلك الضغط على Ctrl + Shift + C لفتح وحدة طرفية جديدة.

- بعد ذلك، تأكَّد من أنّك في دليل العمل gemini-multimodal-chat-assistant، ثم نفِّذ الأمر التالي
uv run backend.py
- في حال النجاح، ستظهر المخرجات على النحو التالي
INFO: Started server process [xxxxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)
شرح الرمز
تحديد مسار HTTP لتلقّي طلب Chat
في FastAPI، نحدّد المسار باستخدام أداة التزيين app. نستخدم أيضًا Pydantic لتحديد عقد واجهة برمجة التطبيقات. نحدّد أنّ مسار إنشاء الردّ هو المسار /chat باستخدام طريقة POST. يتم الإعلان عن هذه الوظائف في الرمز التالي
class FileData(BaseModel):
data: str
mime_type: str
class Message(BaseModel):
role: str
content: str | List[FileData]
class LastUserMessage(BaseModel):
text: str
files: List[FileData] = []
class ChatRequest(BaseModel):
message: LastUserMessage
history: List[Message]
system_prompt: str = DEFAULT_SYSTEM_PROMPT
class ChatResponse(BaseModel):
response: str
error: Optional[str] = None
...
@app.post("/chat", response_model=ChatResponse)
async def chat(
request: ChatRequest = Body(...),
) -> ChatResponse:
# Truncated
...
تجهيز تنسيق سجلّ محادثات Gemini SDK
من الأمور المهمة التي يجب فهمها هي كيفية إعادة هيكلة سجلّ المحادثات حتى يمكن إدراجه كقيمة وسيطة للسجلّ عند إعداد عميل Gemini لاحقًا. يمكنك فحص الرمز أدناه
def format_message_history_to_gemini_standard(
message_history: List[Message],
) -> List[Content]:
...
# Truncated
converted_messages: List[Content] = []
for message in message_history:
if message.role == "assistant":
converted_messages.append(
Content(role="model", parts=[Part.from_text(text=message.content)])
)
elif message.role == "user":
# Text-only messages
if isinstance(message.content, str):
converted_messages.append(
Content(role="user", parts=[Part.from_text(text=message.content)])
)
# Messages with files
elif isinstance(message.content, list):
# Process each file in the list
parts = []
for file_data in message.content:
parts.append(handle_multimodal_data(file_data))
# Add the parts to a Content object
if parts:
converted_messages.append(Content(role="user", parts=parts))
#Truncated
...
return converted_messages
لتوفير سجلّ المحادثات في Gemini SDK، علينا تنسيق البيانات في نوع البيانات List[Content]. يجب أن يتضمّن كل محتوى قيمة دور وأجزاء على الأقل. يشير الدور إلى مصدر الرسالة، سواء كان المستخدم أو النموذج. يشير الجزء إلى الطلب نفسه، حيث يمكن أن يكون نصًا فقط أو مزيجًا من وسائط مختلفة. يمكنك الاطّلاع على كيفية تنظيم وسيطات المحتوى بالتفصيل في هذه المستندات.
التعامل مع البيانات غير النصية ( متعددة الوسائط)
كما ذكرنا سابقًا في قسم الواجهة الأمامية، إحدى طرق إرسال بيانات غير نصية أو متعددة الوسائط هي إرسال البيانات كسلسلة base64. علينا أيضًا تحديد نوع MIME للبيانات حتى يمكن تفسيرها بشكل صحيح، مثلاً تقديم نوع MIME image/jpeg إذا أرسلنا بيانات صورة مع لاحقة .jpg.
يحوّل هذا الجزء من الرمز البرمجي بيانات base64 إلى تنسيق Part.from_bytes من Gemini SDK.
def handle_multimodal_data(file_data: FileData) -> Part:
"""Converts Multimodal data to a Google Gemini Part object.
Args:
file_data: FileData object with base64 data and MIME type.
Returns:
Part: A Google Gemini Part object containing the file data.
"""
data = base64.b64decode(file_data.data) # decode base64 string to bytes
return Part.from_bytes(data=data, mime_type=file_data.mime_type)
5- اختبار التكامل
من المفترض الآن أن يتم تشغيل خدمات متعدّدة في علامة تبويب مختلفة في Cloud Console:
- Frontend service run at port 8080
* Running on local URL: http://0.0.0.0:8080 To create a public link, set `share=True` in `launch()`.
- تشغيل خدمة الخلفية على المنفذ 8081
INFO: Started server process [xxxxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)
في الوضع الحالي، من المفترض أن تتمكّن من إرسال مستنداتك في الدردشة بسلاسة مع المساعد من تطبيق الويب على المنفذ 8080. يمكنك البدء بتجربة هذه الميزة من خلال تحميل الملفات وطرح الأسئلة. يُرجى العلم أنّ بعض أنواع الملفات غير متوافقة بعد وسيؤدي استخدامها إلى ظهور خطأ.
يمكنك أيضًا تعديل تعليمات النظام من حقل المدخلات الإضافية أسفل مربّع النص.

6. النشر على Cloud Run
بالطبع، نريد أن نعرض هذا التطبيق الرائع على الآخرين. ولإجراء ذلك، يمكننا تجميع هذا التطبيق ونشره على Cloud Run كخدمة عامة يمكن للآخرين الوصول إليها. لإجراء ذلك، لنراجع البنية

في هذا الدرس العملي، سنضع كلاً من خدمة الواجهة الأمامية والخلفية في حاوية واحدة. سنحتاج إلى مساعدة supervisord لإدارة كلتا الخدمتين.
أنشئ ملفًا جديدًا، وانقر على ملف -> ملف نصي جديد، ثم انسخ الرمز التالي والصقه، ثم احفظه باسم supervisord.conf.
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
[program:backend]
command=uv run backend.py
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=10
startretries=3
[program:frontend]
command=uv run frontend.py
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=10
startretries=3
بعد ذلك، سنحتاج إلى Dockerfile. انقر على ملف (File) > ملف نصي جديد (New Text File)، وانسخ الرمز التالي والصقه ثم احفظه باسم Dockerfile.
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.6.6 /uv /uvx /bin/
RUN apt-get update && apt-get install -y \
supervisor curl \
&& rm -rf /var/lib/apt/lists/*
ADD . /app
WORKDIR /app
RUN uv sync --frozen
EXPOSE 8080
# Copy supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
في هذه المرحلة، لدينا جميع الملفات اللازمة لنشر تطبيقاتنا على Cloud Run، فلنبدأ عملية النشر. انتقِل إلى "وحدة Cloud Shell الطرفية" وتأكَّد من ضبط المشروع الحالي على مشروعك النشط. وإذا لم يكن كذلك، عليك استخدام الأمر gcloud configure لضبط رقم تعريف المشروع:
gcloud config set project [PROJECT_ID]
بعد ذلك، شغِّل الأمر التالي لنشره على Cloud Run.
gcloud run deploy --source . \
--env-vars-file settings.yaml \
--port 8080 \
--region us-central1
سيُطلب منك إدخال اسم لخدمتك، مثلاً "gemini-multimodal-chat-assistant". بما أنّ لدينا Dockerfile في دليل عمل التطبيق، سيتم إنشاء حاوية Docker ونقلها إلى Artifact Registry. سيطلب منك أيضًا إنشاء مستودع Artifact Registry في المنطقة، لذا عليك الإجابة بـ "Y". قُل أيضًا "نعم" عندما يُطلب منك تحديد ما إذا كنت تريد السماح بالاستدعاءات غير المصادَق عليها. يُرجى العِلم أنّنا نسمح بالوصول غير المصادَق عليه هنا لأنّ هذا التطبيق هو تطبيق تجريبي. ننصحك باستخدام المصادقة المناسبة لتطبيقات المؤسسة والإنتاج.
بعد اكتمال عملية النشر، من المفترض أن تحصل على رابط مشابه لما يلي:
https://gemini-multimodal-chat-assistant-*******.us-central1.run.app
يمكنك المتابعة واستخدام التطبيق من نافذة التصفّح المتخفي أو جهازك الجوّال. من المفترض أن يكون قد تم نشره.
7. التحدي
حان الوقت الآن للتألق وصقل مهاراتك الاستكشافية. هل لديك المهارات اللازمة لتغيير الرمز البرمجي كي يتمكّن "المساعد" من قراءة ملفات صوتية أو ملفات فيديو؟
8. تَنظيم
لتجنُّب تحمّل رسوم في حسابك على Google Cloud مقابل الموارد المستخدَمة في هذا الدرس التطبيقي حول الترميز، اتّبِع الخطوات التالية:
- في Google Cloud Console، انتقِل إلى صفحة إدارة الموارد.
- في قائمة المشاريع، اختَر المشروع الذي تريد حذفه، ثم انقر على حذف.
- في مربّع الحوار، اكتب رقم تعريف المشروع، ثم انقر على إيقاف لحذف المشروع.
- بدلاً من ذلك، يمكنك الانتقال إلى Cloud Run في وحدة التحكّم، واختيار الخدمة التي نشرتها للتو وحذفها.