Aidemy: إنشاء أنظمة متعددة الوكلاء باستخدام LangGraph وEDA والذكاء الاصطناعي التوليدي على Google Cloud

1- مقدمة

مرحبًا إذًا، أنت مهتم بفكرة العملاء الافتراضيين، أي المساعدين الصغار الذين يمكنهم إنجاز المهام نيابةً عنك بدون أن تحرّك ساكنًا، أليس كذلك؟ رائع! لكن لنكن واقعيين، فليس من المنطقي أن يعالج وكيل واحد كل المشاكل، خاصةً عندما تعمل على مشاريع أكبر وأكثر تعقيدًا. من المحتمل أنّك ستحتاج إلى فريق كامل من هؤلاء الأشخاص. وهنا يأتي دور الأنظمة المتعدّدة الوكلاء.

تمنحك الوكلاء، عند استخدام النماذج اللغوية الكبيرة، مرونة لا تُصدّق مقارنةً بالترميز الثابت القديم. ولكن، وكما هو الحال دائمًا، تأتي هذه الميزات مع مجموعة من التحديات الصعبة. وهذا ما سنتناوله بالتفصيل في ورشة العمل هذه.

title

في ما يلي ما يمكنك توقُّعه من هذه الدورة التدريبية، وهي ستساعدك في تحسين مهاراتك في استخدام العملاء الافتراضيين:

إنشاء أول وكيل باستخدام LangGraph: سنتدرّب على إنشاء وكيلك الخاص باستخدام LangGraph، وهو إطار عمل شائع. ستتعرّف على كيفية إنشاء أدوات تتصل بقواعد البيانات، والاستفادة من أحدث إصدار من Gemini 2 API لإجراء بعض عمليات البحث على الإنترنت، وتحسين الطلبات والردود، حتى يتمكّن الوكيل من التفاعل ليس فقط مع النماذج اللغوية الكبيرة، بل مع الخدمات الحالية أيضًا. سنوضّح لك أيضًا طريقة عمل ميزة "استدعاء الدوال".

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

الأنظمة المتعدّدة الوكلاء: ستتعرّف على كيفية إعداد نظام يمكن فيه للوكلاء التعاون وإنجاز المهام معًا، وذلك بفضل بنية مستندة إلى الأحداث.

المرونة في استخدام النماذج اللغوية الكبيرة: استخدام الأفضل للمهمة: لا نلتزم بنموذج لغوي كبير واحد فقط. ستتعرّف على كيفية استخدام نماذج لغوية كبيرة متعددة، وتحديد أدوار مختلفة لها لتعزيز قدرتها على حل المشاكل باستخدام "نماذج تفكير" رائعة.

ما هو المحتوى الديناميكي؟ لا مشكلة: تخيَّل أنّ برنامجك الآلي ينشئ محتوًى ديناميكيًا مخصّصًا لكل مستخدم في الوقت الفعلي. سنوضّح لك كيفية تنفيذ ذلك.

الاستفادة من إمكانات السحابة الإلكترونية مع Google Cloud: لا يقتصر الأمر على مجرد التجربة في دفتر ملاحظات. سنوضّح لك كيفية تصميم نظامك المتعدد الوكلاء ونشره على Google Cloud ليكون جاهزًا للاستخدام في العالم الحقيقي.

سيكون هذا المشروع مثالاً جيدًا على كيفية استخدام جميع الأساليب التي تحدّثنا عنها.

2. الهندسة المعمارية

يمكن أن تكون وظيفة المعلّم أو العمل في مجال التعليم مجزيًا للغاية، ولكن دعونا نواجه الأمر، يمكن أن يكون عبء العمل، خاصةً كل أعمال التحضير، صعبًا. بالإضافة إلى ذلك، لا يتوفّر في أغلب الأحيان عدد كافٍ من الموظفين، وقد تكون الدروس الخصوصية مكلفة. لهذا السبب، نقترح إنشاء مساعد تعليمي مستنِد إلى الذكاء الاصطناعي. يمكن أن تخفّف هذه الأداة العبء عن المعلّمين وتساعد في سدّ الفجوة الناتجة عن نقص الموظفين وعدم توفّر دروس خصوصية بأسعار معقولة.

يمكن لمساعدنا التعليمي المستند إلى الذكاء الاصطناعي إعداد خطط دروس مفصّلة واختبارات ممتعة وملخّصات صوتية سهلة المتابعة ومهام مخصّصة. يتيح ذلك للمعلّمين التركيز على ما يتقنونه، أي التواصل مع الطلاب ومساعدتهم على حب التعلّم.

يتضمّن النظام موقعَين إلكترونيَّين: أحدهما للمعلّمين لإنشاء خطط دروس للأسابيع القادمة،

مخطِّط

وآخر للطلاب للوصول إلى الاختبارات والمراجعات الصوتية والمهام. بوابة

حسنًا، لنستعرض معًا بنية المساعد التعليمي Aidemy. كما ترى، قسّمنا العملية إلى عدة عناصر أساسية تعمل معًا لتحقيق ذلك.

الهندسة المعمارية

العناصر والتقنيات المعمارية الرئيسية:

‫Google Cloud Platform (GCP): هي أساس النظام بأكمله:

  • ‫Vertex AI: تتيح الوصول إلى نماذج اللغة الكبيرة Gemini من Google.
  • ‫Cloud Run: منصة بدون خادم لتفعيل الوكلاء والدوال ضمن حاويات
  • ‫Cloud SQL: قاعدة بيانات PostgreSQL لبيانات المناهج الدراسية
  • ‫Pub/Sub وEventarc: أساس بنية الأحداث، ما يتيح التواصل غير المتزامن بين المكوّنات
  • Cloud Storage: يتم تخزين الملخّصات الصوتية وملفات الواجبات.
  • ‫Secret Manager: تدير بيانات اعتماد قواعد البيانات بشكل آمن.
  • ‫Artifact Registry: تخزِّن صور Docker للوكلاء.
  • ‫Compute Engine: لنشر نموذج لغوي كبير مستضاف ذاتيًا بدلاً من الاعتماد على حلول المورّدين

نماذج اللغات الكبيرة: هي "العقول" التي تدير النظام:

  • نماذج Gemini من Google: (‫Gemini x Pro وGemini x Flash وGemini x Flash Thinking) تُستخدَم في التخطيط للدروس وإنشاء المحتوى وإنشاء HTML ديناميكي وشرح الاختبارات ودمج المهام.
  • ‫DeepSeek: تم استخدامه في مهمة متخصصة وهي إنشاء مهام دراسية ذاتية

LangChain وLangGraph: أُطر لتطوير تطبيقات النماذج اللغوية الكبيرة

  • يسهّل إنشاء مهام معقّدة تتضمّن عدة وكلاء.
  • تتيح التنسيق الذكي للأدوات (طلبات البيانات من واجهة برمجة التطبيقات، واستعلامات قاعدة البيانات، وعمليات البحث على الويب).
  • تطبيق بنية مستندة إلى الأحداث لضمان قابلية النظام للتوسّع والمرونة

في الأساس، تجمع بنيتنا بين قوة النماذج اللغوية الكبيرة والبيانات المنظَّمة والتواصل المستند إلى الأحداث، وكل ذلك يعمل على Google Cloud. يتيح لنا ذلك إنشاء مساعد تعليمي قابل للتطوير وموثوق وفعّال.

3- قبل البدء

في Google Cloud Console، في صفحة اختيار المشروع، اختَر أو أنشِئ مشروعًا على Google Cloud. تأكَّد من تفعيل الفوترة لمشروعك على Cloud. كيفية التحقّق من تفعيل الفوترة في مشروع

تفعيل Gemini Code Assist في بيئة تطوير متكاملة (IDE) في Cloud Shell

👉 في Google Cloud Console، انتقِل إلى "أدوات Gemini Code Assist"، وفعِّل Gemini Code Assist بدون أي تكلفة من خلال الموافقة على البنود والشروط.

01-04-code-assist-enable.png

تجاهل إعداد الأذونات، ثم اترك هذه الصفحة.

العمل على "محرّر Cloud Shell"

👉انقر على تفعيل Cloud Shell في أعلى وحدة تحكّم Google Cloud (رمز شكل الوحدة الطرفية في أعلى لوحة Cloud Shell)، ثم انقر على الزر "فتح المحرّر" (يشبه مجلدًا مفتوحًا مع قلم رصاص). سيؤدي ذلك إلى فتح "محرِّر Cloud Shell" في النافذة. سيظهر لك مستكشف الملفات على الجانب الأيمن.

Cloud Shell

👉انقر على الزر تسجيل الدخول باستخدام رمز السحابة الإلكترونية في شريط الحالة أسفل الصفحة كما هو موضّح. امنح المصادقة للمكوّن الإضافي حسب التعليمات. إذا ظهرت لك الرسالة Cloud Code - no project في شريط الحالة، انقر عليها ثمّ انقر على "اختيار مشروع Google Cloud" في القائمة المنسدلة، ثمّ اختَر مشروع Google Cloud المحدّد من قائمة المشاريع التي أنشأتها.

تسجيل الدخول إلى المشروع

👉افتح المحطة الطرفية في بيئة التطوير المتكاملة المستندة إلى السحابة الإلكترونية، مبنى الركاب الجديد أو مبنى الركاب الجديد

👉في نافذة الأوامر، تأكَّد من أنّك قد أثبتّ هويتك وأنّ المشروع مضبوط على رقم تعريف مشروعك باستخدام الأمر التالي:

gcloud auth list

👉نفِّذ الأمر مع الحرص على استبدال <YOUR_PROJECT_ID> برقم تعريف مشروعك:

echo <YOUR_PROJECT_ID> > ~/project_id.txt
gcloud config set project $(cat ~/project_id.txt)

👉نفِّذ الأمر التالي لتفعيل واجهات برمجة التطبيقات اللازمة في Google Cloud:

gcloud services enable compute.googleapis.com  \
                        storage.googleapis.com  \
                        run.googleapis.com  \
                        artifactregistry.googleapis.com  \
                        aiplatform.googleapis.com \
                        eventarc.googleapis.com \
                        sqladmin.googleapis.com \
                        secretmanager.googleapis.com \
                        cloudbuild.googleapis.com \
                        cloudresourcemanager.googleapis.com \
                        cloudfunctions.googleapis.com \
                        cloudaicompanion.googleapis.com

قد تستغرق هذه العملية بضع دقائق.

إعداد الأذونات

👉إعداد إذن حساب الخدمة في الوحدة الطرفية، شغِّل الأمر التالي :

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")

echo "Here's your SERVICE_ACCOUNT_NAME $SERVICE_ACCOUNT_NAME"

👉 منح الأذونات في الوحدة الطرفية، شغِّل الأمر التالي :

#Cloud Storage (Read/Write):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/storage.objectAdmin"

#Pub/Sub (Publish/Receive):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/pubsub.publisher"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/pubsub.subscriber"


#Cloud SQL (Read/Write):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/cloudsql.editor"


#Eventarc (Receive Events):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/iam.serviceAccountTokenCreator"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/eventarc.eventReceiver"

#Vertex AI (User):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/aiplatform.user"

#Secret Manager (Read):
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
  --role="roles/secretmanager.secretAccessor"

👉التحقّق من النتيجة في وحدة تحكّم إدارة الهوية وإمكانية الوصولوحدة تحكّم إدارة الهوية وإمكانية الوصول

👉نفِّذ الأوامر التالية في الوحدة الطرفية لإنشاء مثيل Cloud SQL باسم aidemy. سنحتاج إلى ذلك لاحقًا، ولكن بما أنّ هذه العملية قد تستغرق بعض الوقت، سننفّذها الآن.

gcloud sql instances create aidemy \
    --database-version=POSTGRES_14 \
    --cpu=2 \
    --memory=4GB \
    --region=us-central1 \
    --root-password=1234qwer \
    --storage-size=10GB \
    --storage-auto-increase

4. إنشاء الوكيل الأول

قبل الخوض في تفاصيل الأنظمة المعقّدة المتعددة العملاء، علينا إنشاء وحدة أساسية: عميل واحد وظيفي. في هذا القسم، سنتّخذ خطواتنا الأولى من خلال إنشاء وكيل بسيط "مزوّد كتب". يأخذ وكيل مقدّم الكتب فئة كمدخل ويستخدم نموذجًا لغويًا كبيرًا من Gemini لإنشاء تمثيل JSON لكتاب ضمن تلك الفئة. بعد ذلك، يعرض اقتراحات الكتب هذه كنقطة نهاية لواجهة برمجة تطبيقات REST .

مقدّم الكتب

👉في علامة تبويب أخرى في المتصفّح، افتح Google Cloud Console في متصفّح الويب. في قائمة التنقّل (☰)، انتقِل إلى Cloud Run. انقر على الزر "+ ... كتابة دالة".

إنشاء دالة

👉بعد ذلك، سنضبط الإعدادات الأساسية لوظيفة Cloud Run:

  • اسم الخدمة: book-provider
  • المنطقة: us-central1
  • وقت التشغيل: Python 3.12
  • المصادقة: Allow unauthenticated invocations إلى "مفعَّلة"

👉اترك الإعدادات الأخرى على الوضع التلقائي وانقر على إنشاء. سينقلك هذا الإجراء إلى أداة تعديل رمز المصدر.

ستظهر لك ملفات main.py وrequirements.txt معبّأة مسبقًا.

سيحتوي main.py على منطق النشاط التجاري للدالة، وسيحتوي requirements.txt على الحِزم المطلوبة.

👉أصبحنا الآن جاهزين لكتابة بعض التعليمات البرمجية! ولكن قبل البدء، لنرَ ما إذا كان بإمكان Gemini Code Assist مساعدتنا في ذلك. ارجع إلى Cloud Shell Editor، وانقر على رمز Gemini Code Assist في أعلى الصفحة، وسيتم فتح محادثة Gemini Code Assist.

Gemini Code Assist

👉 ألصِق الطلب التالي في مربّع الطلب:

Use the functions_framework library to be deployable as an HTTP function. 
Accept a request with category and number_of_book parameters (either in JSON body or query string). 
Use langchain and gemini to generate the data for book with fields bookname, author, publisher, publishing_date. 
Use pydantic to define a Book model with the fields: bookname (string, description: "Name of the book"), author (string, description: "Name of the author"), publisher (string, description: "Name of the publisher"), and publishing_date (string, description: "Date of publishing"). 
Use langchain and gemini model to generate book data. the output should follow the format defined in Book model. 

The logic should use JsonOutputParser from langchain to enforce output format defined in Book Model. 
Have a function get_recommended_books(category) that internally uses langchain and gemini to return a single book object. 
The main function, exposed as the Cloud Function, should call get_recommended_books() multiple times (based on number_of_book) and return a JSON list of the generated book objects. 
Handle the case where category or number_of_book are missing by returning an error JSON response with a 400 status code. 
return a JSON string representing the recommended books. use os library to retrieve GOOGLE_CLOUD_PROJECT env var. Use ChatVertexAI from langchain for the LLM call

بعد ذلك، ستنشئ أداة Code Assist حلاً محتملاً، وستوفّر رمز المصدر وملف التبعية requirements.txt. (لا تستخدِم هذا الرمز)

ننصحك بمقارنة الرمز الذي أنشأته ميزة Code Assist بالحلّ الصحيح الذي تم اختباره والموضّح أدناه. يتيح لك ذلك تقييم فعالية الأداة وتحديد أي اختلافات محتملة. على الرغم من أنّه لا يجب الوثوق بشكل أعمى بالنماذج اللغوية الكبيرة، يمكن أن تكون Code Assist أداة رائعة لإنشاء نماذج أولية بسرعة وإنشاء بنى رموز برمجية أولية، ويجب استخدامها للحصول على بداية جيدة.

بما أنّ هذه ورشة عمل، سنواصل العملية باستخدام الرمز الذي تم إثبات ملكيته والمذكور أدناه. ومع ذلك، يمكنك تجربة الرمز البرمجي الذي أنشأته ميزة "مساعد الترميز" في الوقت الذي يناسبك للتعرّف بشكل أفضل على إمكاناتها وقيودها.

👉ارجِع إلى محرّر رمز المصدر الخاص بدالة Cloud Run (في علامة تبويب المتصفّح الأخرى). استبدِل بعناية المحتوى الحالي لملف main.py بالرمز البرمجي المقدَّم أدناه:

import functions_framework
import json
from flask import Flask, jsonify, request
from langchain_google_vertexai import ChatVertexAI
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from pydantic import BaseModel, Field
import os

class Book(BaseModel):
    bookname: str = Field(description="Name of the book")
    author: str = Field(description="Name of the author")
    publisher: str = Field(description="Name of the publisher")
    publishing_date: str = Field(description="Date of publishing")


project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")  

llm = ChatVertexAI(model_name="gemini-2.0-flash-lite-001")

def get_recommended_books(category):
    """
    A simple book recommendation function. 

    Args:
        category (str): category

    Returns:
        str: A JSON string representing the recommended books.
    """
    parser = JsonOutputParser(pydantic_object=Book)
    question = f"Generate a random made up book on {category} with bookname, author and publisher and publishing_date"

    prompt = PromptTemplate(
        template="Answer the user query.\n{format_instructions}\n{query}\n",
        input_variables=["query"],
        partial_variables={"format_instructions": parser.get_format_instructions()},
    )
    
    chain = prompt | llm | parser
    response = chain.invoke({"query": question})

    return  json.dumps(response)
    

@functions_framework.http
def recommended(request):
    request_json = request.get_json(silent=True) # Get JSON data
    if request_json and 'category' in request_json and 'number_of_book' in request_json:
        category = request_json['category']
        number_of_book = int(request_json['number_of_book'])
    elif request.args and 'category' in request.args and 'number_of_book' in request.args:
        category = request.args.get('category')
        number_of_book = int(request.args.get('number_of_book'))

    else:
        return jsonify({'error': 'Missing category or number_of_book parameters'}), 400


    recommendations_list = []
    for i in range(number_of_book):
        book_dict = json.loads(get_recommended_books(category))
        print(f"book_dict=======>{book_dict}")
    
        recommendations_list.append(book_dict)

    
    return jsonify(recommendations_list)

👉استبدِل محتوى ملف requirements.txt بما يلي:

functions-framework==3.*
google-genai==1.0.0
flask==3.1.0
jsonify==0.5
langchain_google_vertexai==2.0.13
langchain_core==0.3.34
pydantic==2.10.5

👉سنضبط نقطة دخول الدالة على: recommended

03-02-function-create.png

👉انقر على حفظ ونشر (أو حفظ وإعادة نشر) لنشر الدالة. انتظِر إلى أن تكتمل عملية النشر. ستعرض Cloud Console الحالة. قد يستغرِق هذا الإجراء عدّة دقائق.

النص البديل 👉بعد النشر، ارجع إلى محرِّر Cloud Shell، وشغِّل الأمر التالي في الوحدة الطرفية:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")

curl -X POST -H "Content-Type: application/json" -d '{"category": "Science Fiction", "number_of_book": 2}' $BOOK_PROVIDER_URL

يجب أن يعرض بعض بيانات الكتب بتنسيق JSON.

[
  {"author":"Anya Sharma","bookname":"Echoes of the Singularity","publisher":"NovaLight Publishing","publishing_date":"2077-03-15"},
  {"author":"Anya Sharma","bookname":"Echoes of the Quantum Dawn","publisher":"Nova Genesis Publishing","publishing_date":"2077-03-15"}
]

تهانينا! لقد نشرت إحدى دوال Cloud Run بنجاح. هذه إحدى الخدمات التي سنعمل على دمجها عند تطوير وكيل Aidemy.

5- أدوات الإنشاء: ربط الوكلاء بخدمة RESTFUL والبيانات

لننزّل الآن "مشروع Bootstrap Skeleton"، وتأكَّد من أنّك في "محرّر Cloud Shell". في الوحدة الطرفية، شغِّل

git clone https://github.com/weimeilin79/aidemy-bootstrap.git

بعد تنفيذ هذا الأمر، سيتم إنشاء مجلد جديد باسم aidemy-bootstrap في بيئة Cloud Shell.

في لوحة "المستكشف" في "محرّر Cloud Shell" (عادةً على الجانب الأيمن)، من المفترض أن يظهر الآن المجلد الذي تم إنشاؤه عند استنساخ مستودع Git aidemy-bootstrap. افتح المجلد الجذر لمشروعك في "المستكشف". ستجد مجلدًا فرعيًا باسم planner داخله، افتحه أيضًا. مستكشف المشاريع

لنبدأ بتطوير الأدوات التي سيستخدمها الوكلاء ليصبحوا مساعدين فعّالين. كما تعلم، تتفوّق النماذج اللغوية الكبيرة في الاستدلال وإنشاء النصوص، ولكنّها تحتاج إلى الوصول إلى مصادر خارجية لتنفيذ المهام في العالم الحقيقي وتقديم معلومات دقيقة وحديثة. يمكن تشبيه هذه الأدوات بـ "سكين الجيش السويسري" الخاص بالوكيل، فهي تمنحه القدرة على التفاعل مع العالم.

عند إنشاء وكيل، من السهل الوقوع في خطأ الترميز الثابت للكثير من التفاصيل. يؤدي ذلك إلى إنشاء وكيل غير مرن. بدلاً من ذلك، من خلال إنشاء الأدوات واستخدامها، يمكن للوكيل الوصول إلى الأنظمة أو المنطق الخارجي، ما يمنحه مزايا كلّ من النموذج اللغوي الكبير والبرمجة التقليدية.

في هذا القسم، سنضع الأساس لوكيل التخطيط الذي سيستخدمه المعلّمون لإنشاء خطط الدروس. وقبل أن يبدأ الوكيل في إنشاء خطة، نريد وضع حدود من خلال تقديم المزيد من التفاصيل حول الموضوع. سننشئ ثلاث أدوات:

  1. طلب بيانات من واجهة برمجة تطبيقات RESTful: التفاعل مع واجهة برمجة تطبيقات حالية لاسترداد البيانات
  2. طلب البحث في قاعدة البيانات: يتم استرداد البيانات المنظَّمة من قاعدة بيانات Cloud SQL.
  3. بحث Google: الوصول إلى المعلومات في الوقت الفعلي من الويب

استرداد اقتراحات الكتب من واجهة برمجة تطبيقات

أولاً، لننشئ أداة تسترد اقتراحات الكتب من واجهة برمجة التطبيقات book-provider التي نشرناها في القسم السابق. يوضّح هذا المثال كيف يمكن للوكيل الاستفادة من الخدمات الحالية.

اقتراح كتاب

في "محرِّر Cloud Shell"، افتح المشروع aidemy-bootstrap الذي استنسخته في القسم السابق.

👉عدِّل book.py في المجلد planner، وألصِق الرمز التالي في نهاية الملف:

def recommend_book(query: str):
    """
    Get a list of recommended book from an API endpoint
    
    Args:
        query: User's request string
    """

    region = get_next_region();
    llm = VertexAI(model_name="gemini-1.5-pro", location=region)

    query = f"""The user is trying to plan a education course, you are the teaching assistant. Help define the category of what the user requested to teach, respond the categroy with no more than two word.

    user request:   {query}
    """
    print(f"-------->{query}")
    response = llm.invoke(query)
    print(f"CATEGORY RESPONSE------------>: {response}")
    
    # call this using python and parse the json back to dict
    category = response.strip()
    
    headers = {"Content-Type": "application/json"}
    data = {"category": category, "number_of_book": 2}

    books = requests.post(BOOK_PROVIDER_URL, headers=headers, json=data)
   
    return books.text

if __name__ == "__main__":
    print(recommend_book("I'm doing a course for my 5th grade student on Math Geometry, I'll need to recommend few books come up with a teach plan, few quizes and also a homework assignment."))

الشرح:

  • recommend_book(query: str): تأخذ هذه الدالة طلب بحث المستخدم كمدخل.
  • التفاعل مع النموذج اللغوي الكبير: يستخدم هذا المكوّن النموذج اللغوي الكبير لاستخراج الفئة من طلب البحث. يوضّح هذا المثال كيف يمكنك استخدام النموذج اللغوي الكبير للمساعدة في إنشاء مَعلمات للأدوات.
  • طلب البيانات من واجهة برمجة التطبيقات: يتم إرسال طلب POST إلى واجهة برمجة التطبيقات الخاصة بمزوّد الكتب، مع تمرير الفئة وعدد الكتب المطلوب.

👉لاختبار هذه الدالة الجديدة، اضبط متغيّر البيئة، ثم شغِّل الأمر التالي :

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
cd ~/aidemy-bootstrap/planner/
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")

👉ثبِّت التبعيات وشغِّل الرمز للتأكّد من أنّه يعمل، شغِّل:

cd ~/aidemy-bootstrap/planner/
python -m venv env
source env/bin/activate
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
pip install -r requirements.txt
python book.py

ستظهر لك سلسلة JSON تحتوي على اقتراحات كتب تم استردادها من واجهة برمجة التطبيقات الخاصة بمزوّد الكتب. يتم إنشاء النتائج بشكل عشوائي. قد لا تكون الكتب متطابقة، ولكن من المفترض أن تتلقّى اقتراحَين بتنسيق JSON.

[{"author":"Anya Sharma","bookname":"Echoes of the Singularity","publisher":"NovaLight Publishing","publishing_date":"2077-03-15"},{"author":"Anya Sharma","bookname":"Echoes of the Quantum Dawn","publisher":"Nova Genesis Publishing","publishing_date":"2077-03-15"}]

إذا ظهرت لك هذه الرسالة، يعني ذلك أنّ الأداة الأولى تعمل بشكل صحيح.

بدلاً من إنشاء طلب RESTful API بشكل صريح باستخدام مَعلمات محدّدة، نستخدم اللغة الطبيعية ("أشارك في دورة تدريبية..."). يستخرج الوكيل بعد ذلك المَعلمات اللازمة (مثل الفئة) بذكاء باستخدام معالجة اللغة الطبيعية، ما يوضّح كيف يستفيد الوكيل من فهم اللغة الطبيعية للتفاعل مع واجهة برمجة التطبيقات.

مكالمة مقارنة

👉أزِل رمز الاختبار التالي من book.py

if __name__ == "__main__":
    print(recommend_book("I'm doing a course for my 5th grade student on Math Geometry, I'll need to recommend few books come up with a teach plan, few quizes and also a homework assignment."))

الحصول على بيانات المناهج من قاعدة بيانات

بعد ذلك، سننشئ أداة تسترد بيانات المناهج المنظَّمة من قاعدة بيانات Cloud SQL PostgreSQL. يتيح ذلك للوكيل الوصول إلى مصدر موثوق للمعلومات من أجل التخطيط للدروس.

create db

هل تتذكّر مثيل Cloud SQL الذي أنشأته في الخطوة السابقة باسم aidemy؟ في ما يلي الأماكن التي سيتم استخدامها فيها.

👉 في الوحدة الطرفية، شغِّل الأمر التالي لإنشاء قاعدة بيانات باسم aidemy-db في المثيل الجديد.

gcloud sql databases create aidemy-db \
    --instance=aidemy

لنتأكّد من المثيل في Cloud SQL في Google Cloud Console. من المفترض أن يظهر لك مثيل Cloud SQL باسم aidemy.

👉 انقر على اسم الجهاز الافتراضي لعرض تفاصيله. 👉 في صفحة تفاصيل مثيل Cloud SQL، انقر على Cloud SQL Studio في قائمة التنقّل اليمنى. سيؤدي ذلك إلى فتح علامة تبويب جديدة.

اختَر aidemy-db كقاعدة البيانات، وأدخِل postgres كـ مستخدم و1234qwer كـ كلمة مرور.

انقر على مصادقة.

تسجيل الدخول إلى SQL Studio

👉في أداة تعديل طلبات البحث في SQL Studio، انتقِل إلى علامة التبويب المحرّر 1، وألصِق رمز SQL التالي:

CREATE TABLE curriculums (
    id SERIAL PRIMARY KEY,
    year INT,
    subject VARCHAR(255),
    description TEXT
);

-- Inserting detailed curriculum data for different school years and subjects
INSERT INTO curriculums (year, subject, description) VALUES
-- Year 5
(5, 'Mathematics', 'Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.'),
(5, 'English', 'Developing reading comprehension, creative writing, and basic grammar, with a focus on storytelling and poetry.'),
(5, 'Science', 'Exploring basic physics, chemistry, and biology concepts, including forces, materials, and ecosystems.'),
(5, 'Computer Science', 'Basic coding concepts using block-based programming and an introduction to digital literacy.'),

-- Year 6
(6, 'Mathematics', 'Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.'),
(6, 'English', 'Introduction to persuasive writing, character analysis, and deeper comprehension of literary texts.'),
(6, 'Science', 'Forces and motion, the human body, and introductory chemical reactions with hands-on experiments.'),
(6, 'Computer Science', 'Introduction to algorithms, logical reasoning, and basic text-based programming (Python, Scratch).'),

-- Year 7
(7, 'Mathematics', 'Algebraic expressions, geometry, and introduction to statistics and probability.'),
(7, 'English', 'Analytical reading of classic and modern literature, essay writing, and advanced grammar skills.'),
(7, 'Science', 'Introduction to cells and organisms, chemical reactions, and energy transfer in physics.'),
(7, 'Computer Science', 'Building on programming skills with Python, introduction to web development, and cyber safety.');

ينشئ رمز SQL هذا جدولاً باسم curriculums ويدرج بعض البيانات النموذجية.

👉 انقر على تشغيل لتنفيذ رمز SQL. من المفترض أن تظهر لك رسالة تأكيد تشير إلى أنّه تم تنفيذ العبارات بنجاح.

👉 وسِّع المستكشف، وابحث عن الجدول الذي تم إنشاؤه حديثًا curriculums وانقر على طلب بحث. من المفترض أن يتم فتح علامة تبويب جديدة للمحرّر تحتوي على عبارة SQL تم إنشاؤها لك.

sql studio select table

SELECT * FROM
  "public"."curriculums" LIMIT 1000;

👉انقر على تشغيل.

يجب أن يعرض جدول النتائج صفوف البيانات التي أدرجتها في الخطوة السابقة، ما يؤكّد أنّه تم إنشاء الجدول والبيانات بشكل صحيح.

بعد أن أنشأت بنجاح قاعدة بيانات تتضمّن بيانات المناهج الدراسية النموذجية، سننشئ أداة لاستردادها.

👉في "أداة تعديل الرموز البرمجية على السحابة الإلكترونية"، عدِّل الملف curriculums.py في المجلد aidemy-bootstrap وألصِق الرمز التالي في نهاية الملف:

def connect_with_connector() -> sqlalchemy.engine.base.Engine:

    db_user = os.environ["DB_USER"]
    db_pass = os.environ["DB_PASS"]
    db_name = os.environ["DB_NAME"]

    print(f"--------------------------->db_user: {db_user!r}")
    print(f"--------------------------->db_pass: {db_pass!r}")
    print(f"--------------------------->db_name: {db_name!r}")

    connector = Connector()

    pool = sqlalchemy.create_engine(
        "postgresql+pg8000://",
        creator=lambda: connector.connect(
            instance_connection_name,
            "pg8000",
            user=db_user,
            password=db_pass,
            db=db_name,
        ),
        pool_size=2,
        max_overflow=2,
        pool_timeout=30,  # 30 seconds
        pool_recycle=1800,  # 30 minutes
    )
    return pool

def get_curriculum(year: int, subject: str):
    """
    Get school curriculum

    Args:
        subject: User's request subject string
        year: User's request year int
    """
    try:
        stmt = sqlalchemy.text(
            "SELECT description FROM curriculums WHERE year = :year AND subject = :subject"
        )

        with db.connect() as conn:
            result = conn.execute(stmt, parameters={"year": year, "subject": subject})
            row = result.fetchone()
        if row:
            return row[0]
        else:
            return None

    except Exception as e:
        print(e)
        return None

db = connect_with_connector()

الشرح:

  • متغيّرات البيئة: يسترجع الرمز البرمجي بيانات اعتماد قاعدة البيانات ومعلومات الاتصال من متغيّرات البيئة (مزيد من المعلومات أدناه).
  • connect_with_connector(): تستخدم هذه الدالة Cloud SQL Connector لإنشاء اتصال آمن بقاعدة البيانات.
  • get_curriculum(year: int, subject: str): تستخدم هذه الدالة السنة والمادة كمدخلات، وتطلب جدول المناهج الدراسية، وتعرض وصف المنهج الدراسي المناسب.

👉قبل أن نتمكّن من تشغيل الرمز، يجب ضبط بعض متغيرات البيئة، وفي المحطة الطرفية، شغِّل:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"

👉لاختبار الإضافة، أضِف الرمز التالي إلى نهاية curriculums.py:

if __name__ == "__main__":
    print(get_curriculum(6, "Mathematics"))

👉تشغيل الرمز البرمجي:

cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python curriculums.py

يجب أن يظهر وصف المنهج الدراسي لمادة الرياضيات للصف السادس في وحدة التحكّم.

Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.

إذا ظهر لك وصف المنهج الدراسي، يعني ذلك أنّ أداة قاعدة البيانات تعمل بشكل صحيح. يُرجى إيقاف النص البرمجي بالضغط على Ctrl+C إذا كان لا يزال قيد التشغيل.

👉أزِل رمز الاختبار التالي من curriculums.py

if __name__ == "__main__":
    print(get_curriculum(6, "Mathematics"))

👉للخروج من البيئة الافتراضية، شغِّل الأمر التالي في الوحدة الطرفية:

deactivate

6. أدوات البناء: الوصول إلى معلومات في الوقت الفعلي من الويب

أخيرًا، سننشئ أداة تستخدم تكامل Gemini 2 مع "بحث Google" للوصول إلى معلومات في الوقت الفعلي من الويب. يساعد ذلك الوكيل على البقاء على اطّلاع دائم وتقديم نتائج ذات صلة.

يؤدي دمج Gemini 2 مع واجهة برمجة التطبيقات الخاصة بمحرّك بحث Google إلى تحسين إمكانات الوكيل من خلال تقديم نتائج بحث أكثر دقة وملاءمة للسياق. يتيح ذلك للوكلاء الوصول إلى معلومات حديثة وتحديد مصادر ردودهم من بيانات واقعية، ما يقلّل من الهلوسة. يسهّل دمج واجهة برمجة التطبيقات المحسّن أيضًا إجراء المزيد من طلبات البحث باللغة اليومية العادية، ما يتيح للوكلاء صياغة طلبات بحث معقّدة ودقيقة.

بحث

تتلقّى هذه الدالة طلب بحث ومنهجًا وموضوعًا وسنة كمدخلات، وتستخدم واجهة Gemini API وأداة "بحث Google" لاسترداد المعلومات ذات الصلة من الإنترنت. إذا دقّقت النظر، ستلاحظ أنّه يستخدم حزمة تطوير البرامج (SDK) للذكاء الاصطناعي التوليدي من Google لتنفيذ طلبات الدوال بدون استخدام أي إطار عمل آخر.

👉عدِّل search.py في المجلد aidemy-bootstrap وألصِق الرمز التالي في نهاية الملف:

model_id = "gemini-2.0-flash-001"

google_search_tool = Tool(
    google_search = GoogleSearch()
)

def search_latest_resource(search_text: str, curriculum: str, subject: str, year: int):
    """
    Get latest information from the internet
    
    Args:
        search_text: User's request category   string
        subject: "User's request subject" string
        year: "User's request year"  integer
    """
    search_text = "%s in the context of year %d and subject %s with following curriculum detail %s " % (search_text, year, subject, curriculum)
    region = get_next_region()
    client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
    print(f"search_latest_resource text-----> {search_text}")
    response = client.models.generate_content(
        model=model_id,
        contents=search_text,
        config=GenerateContentConfig(
            tools=[google_search_tool],
            response_modalities=["TEXT"],
        )
    )
    print(f"search_latest_resource response-----> {response}")
    return response

if __name__ == "__main__":
  response = search_latest_resource("What are the syllabus for Year 6 Mathematics?", "Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.", "Mathematics", 6)
  for each in response.candidates[0].content.parts:
    print(each.text)

الشرح:

  • أداة التعريف - google_search_tool: تضمين عنصر GoogleSearch ضمن أداة
  • search_latest_resource(search_text: str, subject: str, year: int): تأخذ هذه الدالة طلب بحث وموضوعًا وسنة كمدخلات، وتستخدم Gemini API لإجراء بحث على Google.
  • GenerateContentConfig: تحديد أنّ لديه إذن الوصول إلى أداة GoogleSearch

يحلّل نموذج Gemini داخليًا النص_البحث ويحدّد ما إذا كان بإمكانه الإجابة عن السؤال مباشرةً أو ما إذا كان بحاجة إلى استخدام أداة GoogleSearch. هذه خطوة مهمة تحدث ضمن عملية الاستدلال التي يجريها النموذج اللغوي الكبير. تم تدريب النموذج على التعرّف على الحالات التي تتطلّب استخدام أدوات خارجية. إذا قرر النموذج استخدام أداة GoogleSearch، تتولّى حزمة تطوير البرامج (SDK) من Google للذكاء الاصطناعي التوليدي عملية الاستدعاء الفعلية. تأخذ حزمة SDK قرار النموذج والمعلَمات التي ينشئها وترسلها إلى Google Search API. هذا الجزء مخفي عن المستخدم في الرمز.

بعد ذلك، يدمج نموذج Gemini نتائج البحث في رده. ويمكنه استخدام المعلومات للإجابة عن سؤال المستخدم أو إنشاء ملخّص أو تنفيذ مهمة أخرى.

👉لاختبار ذلك، شغِّل الرمز البرمجي:

cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
source env/bin/activate
python search.py

من المفترض أن يظهر لك الردّ من Gemini Search API الذي يتضمّن نتائج بحث ذات صلة بـ "منهج الرياضيات للصف الخامس". ستختلف النتيجة الدقيقة حسب نتائج البحث، ولكنها ستكون كائن JSON يتضمّن معلومات حول البحث.

إذا ظهرت لك نتائج بحث، يعني ذلك أنّ أداة "بحث Google" تعمل بشكل صحيح. يمكنك إيقاف البرنامج النصي بالضغط على Ctrl+C إذا كان لا يزال قيد التشغيل.

👉أزِل الجزء الأخير من الرمز.

if __name__ == "__main__":
  response = search_latest_resource("What are the syllabus for Year 6 Mathematics?", "Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.", "Mathematics", 6)
  for each in response.candidates[0].content.parts:
    print(each.text)

👈للخروج من البيئة الافتراضية، نفِّذ الأمر التالي في الوحدة الطرفية:

deactivate

تهانينا! لقد أنشأت الآن ثلاث أدوات فعّالة لوكيل التخطيط: موصّل واجهة برمجة التطبيقات وموصّل قاعدة البيانات وأداة "بحث Google". ستتيح هذه الأدوات للوكيل الوصول إلى المعلومات والإمكانات التي يحتاجها لإنشاء خطط تعليمية فعّالة.

7. تنظيم المهام باستخدام LangGraph

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

‫LangGraph هي مكتبة Python مصمَّمة لتسهيل إنشاء تطبيقات متعددة الجهات الفاعلة ومستندة إلى الحالة باستخدام النماذج اللغوية الكبيرة (LLM). يمكنك اعتبارها إطار عمل لتنظيم المحادثات وسير العمل المعقّدة التي تتضمّن نماذج لغوية كبيرة وأدوات ووكلاء آخرين.

المفاهيم الأساسية:

  • بنية الرسم البياني: تمثّل LangGraph منطق تطبيقك في شكل رسم بياني موجّه. تمثّل كل عُقدة في الرسم البياني خطوة في العملية (مثل طلب إلى نموذج لغوي كبير، أو استدعاء أداة، أو فحص شرطي). تحدّد الحواف مسار التنفيذ بين العُقد.
  • الحالة: تدير LangGraph حالة تطبيقك أثناء تنقّله في الرسم البياني. يمكن أن تتضمّن هذه الحالة متغيّرات مثل إدخال المستخدم ونتائج طلبات الأدوات والنتائج الوسيطة من النماذج اللغوية الكبيرة وأي معلومات أخرى يجب الاحتفاظ بها بين الخطوات.
  • العُقد: تمثّل كل عقدة عملية حسابية أو تفاعلاً. يمكن أن تكون هذه الأذونات:
    • عُقد الأدوات: استخدام أداة (مثل إجراء بحث على الويب أو طلب بحث من قاعدة بيانات)
    • عُقد الدوال: تنفيذ دالة Python
  • الحواف: تربط العُقد، وتحدّد مسار التنفيذ. يمكن أن تكون هذه الأذونات:
    • الحواف المباشرة: هي مسار بسيط وغير مشروط من عقدة إلى أخرى.
    • الحواف الشرطية: يعتمد التدفق على نتيجة عقدة شرطية.

LangGraph

سنستخدم LangGraph لتنفيذ التنسيق. لنعدّل ملف aidemy.py ضمن مجلد aidemy-bootstrap لتحديد منطق LangGraph.

👉 أضِف الرمز التالي إلى نهاية

aidemy.py:

tools = [get_curriculum, search_latest_resource, recommend_book]

def determine_tool(state: MessagesState):
    llm = ChatVertexAI(model_name="gemini-2.0-flash-001", location=get_next_region())
    sys_msg = SystemMessage(
                    content=(
                        f"""You are a helpful teaching assistant that helps gather all needed information. 
                            Your ultimate goal is to create a detailed 3-week teaching plan. 
                            You have access to tools that help you gather information.  
                            Based on the user request, decide which tool(s) are needed. 

                        """
                    )
                )

    llm_with_tools = llm.bind_tools(tools)
    return {"messages": llm_with_tools.invoke([sys_msg] + state["messages"])} 

هذه الدالة مسؤولة عن أخذ الحالة الحالية للمحادثة، وتزويد النموذج اللغوي الكبير برسالة نظام، ثم الطلب من النموذج اللغوي الكبير إنشاء ردّ. يمكن للنموذج اللغوي الكبير إما الردّ مباشرةً على المستخدم أو اختيار استخدام إحدى الأدوات المتاحة.

الأدوات : تمثّل هذه القائمة مجموعة الأدوات المتاحة للوكيل. يحتوي على ثلاث دوال أدوات حدّدناها في الخطوات السابقة: get_curriculum وsearch_latest_resource وrecommend_book. ‫llm.bind_tools(tools): تعمل هذه الدالة على "ربط" قائمة الأدوات بكائن النموذج اللغوي الكبير. يُعلم ربط الأدوات النموذج اللغوي الكبير بأنّ هذه الأدوات متاحة، كما يزوّده بمعلومات حول كيفية استخدامها (مثل أسماء الأدوات والمعلَمات التي تقبلها وما تفعله).

سنستخدم LangGraph لتنفيذ التنسيق.

👉 أضِف الرمز التالي إلى نهاية

aidemy.py:

def prep_class(prep_needs):
   
    builder = StateGraph(MessagesState)
    builder.add_node("determine_tool", determine_tool)
    builder.add_node("tools", ToolNode(tools))
    
    builder.add_edge(START, "determine_tool")
    builder.add_conditional_edges("determine_tool",tools_condition)
    builder.add_edge("tools", "determine_tool")

    
    memory = MemorySaver()
    graph = builder.compile(checkpointer=memory)

    config = {"configurable": {"thread_id": "1"}}
    messages = graph.invoke({"messages": prep_needs},config)
    print(messages)
    for m in messages['messages']:
        m.pretty_print()
    teaching_plan_result = messages["messages"][-1].content  


    return teaching_plan_result

if __name__ == "__main__":
  prep_class("I'm doing a course for  year 5 on subject Mathematics in Geometry, , get school curriculum, and come up with few books recommendation plus  search latest resources on the internet base on the curriculum outcome. And come up with a 3 week teaching plan")

الشرح:

  • StateGraph(MessagesState): تنشئ هذه السمة عنصر StateGraph. StateGraph هي مفهوم أساسي في LangGraph. وهو يمثّل سير عمل برنامجك كرسم بياني، حيث تمثّل كل عُقدة في الرسم البياني خطوة في العملية. يمكن اعتبارها بمثابة تحديد المخطط الأساسي لطريقة تفكير الوكيل وتصرفه.
  • الحافة الشرطية: تنشأ الحافة الشرطية من العقدة "determine_tool"، ومن المحتمل أن تكون الوسيطة tools_condition دالة تحدد الحافة التي يجب اتّباعها استنادًا إلى ناتج الدالة determine_tool. تسمح الحواف الشرطية بتفرّع الرسم البياني استنادًا إلى قرار النموذج اللغوي الكبير بشأن الأداة التي سيتم استخدامها (أو ما إذا كان سيستجيب للمستخدم مباشرةً). وهنا يأتي دور "ذكاء" الوكيل، إذ يمكنه تعديل سلوكه بشكل ديناميكي استنادًا إلى الموقف.
  • التكرار: يضيف حافة إلى الرسم البياني تربط العقدة "tools" بالعقدة "determine_tool". يؤدي ذلك إلى إنشاء حلقة في الرسم البياني، ما يسمح للوكيل باستخدام الأدوات بشكل متكرر إلى أن يجمع معلومات كافية لإكمال المهمة وتقديم إجابة مرضية. هذه الحلقة ضرورية للمهام المعقّدة التي تتطلّب خطوات متعدّدة من الاستدلال وجمع المعلومات.

الآن، لنختبر وكيل التخطيط لمعرفة كيفية تنسيقه للأدوات المختلفة.

سيشغّل هذا الرمز الدالة prep_class مع إدخال محدّد من المستخدم، ما يحاكي طلبًا لإنشاء خطة تدريس لمادة الرياضيات في الصف الخامس في الهندسة، باستخدام المنهج الدراسي واقتراحات الكتب وأحدث المراجع على الإنترنت.

👉 في الوحدة الطرفية، إذا أغلقتها أو لم تعُد متغيّرات البيئة مضبوطة، أعِد تنفيذ الأوامر التالية

export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"

👉تشغيل الرمز البرمجي:

cd ~/aidemy-bootstrap/planner/
source env/bin/activate
pip install -r requirements.txt
python aidemy.py

شاهِد السجلّ في المحطة الطرفية. يجب أن تظهر لك أدلة على أنّ الوكيل يستدعي جميع الأدوات الثلاث (الحصول على المنهج الدراسي، والحصول على اقتراحات بشأن الكتب، والبحث عن أحدث المراجع) قبل تقديم خطة التدريس النهائية. يوضّح هذا أنّ عملية التنسيق في LangGraph تعمل بشكل صحيح، وأنّ الوكيل يستخدم بذكاء جميع الأدوات المتاحة لتلبية طلب المستخدم.

================================ Human Message =================================

I'm doing a course for  year 5 on subject Mathematics in Geometry, , get school curriculum, and come up with few books recommendation plus  search latest resources on the internet base on the curriculum outcome. And come up with a 3 week teaching plan
================================== Ai Message ==================================
Tool Calls:
  get_curriculum (xxx)
 Call ID: xxx
  Args:
    year: 5.0
    subject: Mathematics
================================= Tool Message =================================
Name: get_curriculum

Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.
================================== Ai Message ==================================
Tool Calls:
  search_latest_resource (xxxx)
 Call ID: xxxx
  Args:
    year: 5.0
    search_text: Geometry
    curriculum: {"content": "Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques."}
    subject: Mathematics
================================= Tool Message =================================
Name: search_latest_resource

candidates=[Candidate(content=Content(parts=[Part(.....) automatic_function_calling_history=[] parsed=None
================================== Ai Message ==================================
Tool Calls:
  recommend_book (93b48189-4d69-4c09-a3bd-4e60cdc5f1c6)
 Call ID: 93b48189-4d69-4c09-a3bd-4e60cdc5f1c6
  Args:
    query: Mathematics Geometry Year 5
================================= Tool Message =================================
Name: recommend_book

[{.....}]

================================== Ai Message ==================================

Based on the curriculum outcome, here is a 3-week teaching plan for year 5 Mathematics Geometry:

**Week 1: Introduction to Shapes and Properties**
.........

أوقِف النص البرمجي بالضغط على Ctrl+C إذا كان لا يزال قيد التشغيل.

👉 (هذه الخطوة اختيارية) استبدِل رمز الاختبار بطلب مختلف يتطلّب استدعاء أدوات مختلفة.

if __name__ == "__main__":
  prep_class("I'm doing a course for year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan")

👉 إذا أغلقت نافذة الجهاز أو لم تعُد متغيّرات البيئة مضبوطة، أعِد تنفيذ الأوامر التالية

gcloud config set project $(cat ~/project_id.txt)
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"

👉 (هذه الخطوة اختيارية، نفِّذها فقط إذا نفّذت الخطوة السابقة) شغِّل الرمز مرة أخرى:

cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python aidemy.py

ماذا لاحظت هذه المرة؟ ما هي الأدوات التي استدعاها الوكيل؟ من المفترض أن تلاحظ أنّ الوكيل يستدعي أداة search_latest_resource فقط هذه المرة. يعود السبب إلى أنّ الطلب لا يحدّد أنّه يحتاج إلى الأداتَين الأخريَين، ونموذج اللغة الكبير (LLM) الذي نستخدمه ذكي بما يكفي لعدم استدعاء الأداتَين الأخريَين.

================================ Human Message =================================

I'm doing a course for  year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan
================================== Ai Message ==================================
Tool Calls:
  get_curriculum (xxx)
 Call ID: xxx
  Args:
    year: 5.0
    subject: Mathematics
================================= Tool Message =================================
Name: get_curriculum

Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.
================================== Ai Message ==================================
Tool Calls:
  search_latest_resource (xxx)
 Call ID: xxxx
  Args:
    year: 5.0
    subject: Mathematics
    curriculum: {"content": "Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques."}
    search_text: Geometry
================================= Tool Message =================================
Name: search_latest_resource

candidates=[Candidate(content=Content(parts=[Part(.......token_count=40, total_token_count=772) automatic_function_calling_history=[] parsed=None
================================== Ai Message ==================================

Based on the information provided, a 3-week teaching plan for Year 5 Mathematics focusing on Geometry could look like this:

**Week 1:  Introducing 2D Shapes**
........
* Use visuals, manipulatives, and real-world examples to make the learning experience engaging and relevant.

أوقِف النص البرمجي بالضغط على Ctrl+C.

👉 (يجب عدم تخطّي هذه الخطوة!) أزِل رمز الاختبار للحفاظ على ملف aidemy.py نظيفًا :

if __name__ == "__main__":
  prep_class("I'm doing a course for  year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan")

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

👉 إذا أغلقت نافذة الجهاز أو لم تعُد متغيّرات البيئة مضبوطة، أعِد تنفيذ الأوامر التالية

export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"

👉 الآن، ابدأ واجهة مستخدم الويب.

cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python app.py

ابحث عن رسائل بدء التشغيل في ناتج وحدة طرفية Cloud Shell. عادةً ما تطبع Flask رسائل تشير إلى أنّها تعمل وإلى المنفذ الذي تعمل عليه.

Running on http://127.0.0.1:8080
Running on http://127.0.0.1:8080
The application needs to keep running to serve requests.

👉 من قائمة "معاينة الويب" في أعلى يسار الصفحة، اختَر المعاينة على المنفذ 8080. سيفتح Cloud Shell علامة تبويب أو نافذة متصفّح جديدة تتضمّن معاينة الويب لتطبيقك.

صفحة ويب

في واجهة التطبيق، اختَر 5 للسنة، واختَر الموضوع Mathematics، واكتب Geometry في "طلب الإضافة".

👉 إذا انتقلت إلى مكان آخر من واجهة مستخدم تطبيقك، ارجع إليها وسيظهر لك الناتج الذي تم إنشاؤه.

👉 في الوحدة الطرفية، أوقِف النص البرمجي بالضغط على Ctrl+C.

👉 في نافذة Terminal، اخرج من البيئة الافتراضية:

deactivate

8. نشر وكيل التخطيط على السحابة الإلكترونية

إنشاء صورة ونقلها إلى السجلّ

نظرة عامة

حان وقت نشر هذا التطبيق على السحابة الإلكترونية.

👉 في الوحدة الطرفية، أنشئ مستودعًا للعناصر لتخزين صورة Docker التي سننشئها.

gcloud artifacts repositories create agent-repository \
    --repository-format=docker \
    --location=us-central1 \
    --description="My agent repository"

من المفترض أن يظهر لك Created repository [agent-repository].

👉 شغِّل الأمر التالي لإنشاء صورة Docker.

cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .

👉 يجب إعادة وضع علامة على الصورة ليتم استضافتها في Artifact Registry بدلاً من GCR، ثم إرسال الصورة التي تم وضع علامة عليها إلى Artifact Registry:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner

بعد اكتمال عملية النقل، يمكنك التأكّد من تخزين الصورة بنجاح في Artifact Registry.

👉 انتقِل إلى Artifact Registry في Google Cloud Console. من المفترض أن تجد صورة aidemy-planner ضمن مستودع agent-repository. صورة &quot;مخطّط Aidemy&quot;

تأمين بيانات اعتماد قاعدة البيانات باستخدام Secret Manager

سنستخدم خدمة Google Cloud Secret Manager لإدارة بيانات اعتماد قواعد البيانات والوصول إليها بأمان. يمنع ذلك تضمين المعلومات الحسّاسة في رمز تطبيقنا ويعزّز الأمان.

سننشئ أسرارًا فردية لاسم مستخدم قاعدة البيانات وكلمة المرور واسم قاعدة البيانات. تتيح لنا هذه الطريقة إدارة كل بيانات اعتماد بشكل مستقل.

👉 في الوحدة الطرفية، نفِّذ ما يلي:

gcloud secrets create db-user
printf "postgres" | gcloud secrets versions add db-user --data-file=-

gcloud secrets create db-pass
printf "1234qwer" | gcloud secrets versions add db-pass --data-file=- 

gcloud secrets create db-name
printf "aidemy-db" | gcloud secrets versions add db-name --data-file=-

يُعدّ استخدام Secret Manager خطوة مهمة في تأمين تطبيقك ومنع الكشف عن بيانات الاعتماد الحسّاسة عن طريق الخطأ. وهي تتّبع أفضل ممارسات الأمان لعمليات النشر على السحابة الإلكترونية.

النشر على Cloud Run

‫Cloud Run هي منصة مُدارة بالكامل بدون خادم تتيح لك نشر التطبيقات في حاويات بسرعة وسهولة. وهي تجرّد إدارة البنية الأساسية، ما يتيح لك التركيز على كتابة الرموز البرمجية ونشرها. سننشر تطبيق التخطيط كخدمة Cloud Run.

👉في Google Cloud Console، انتقِل إلى Cloud Run. انقر على نشر الحاوية واختَر SERVICE. اضبط إعدادات خدمة Cloud Run:

Cloud Run

  1. صورة الحاوية: انقر على "اختيار" في حقل عنوان URL. ابحث عن عنوان URL للصورة التي أرسلتها إلى Artifact Registry (مثلاً، us-central1-docker.pkg.dev/YOUR_PROJECT_ID/agent-repository/aidemy-planner/YOUR_IMG).
  2. اسم الخدمة: aidemy-planner
  3. المنطقة: اختَر المنطقة us-central1.
  4. المصادقة: لأغراض ورشة العمل هذه، يمكنك السماح بـ "السماح باستدعاءات غير مصادَق عليها". بالنسبة إلى الإصدار العلني، من المحتمل أنّك تريد حظر الوصول.
  5. وسِّع قسم "الحاويات" و"وحدات التخزين" و"الشبكات" و"الأمان"، واضبط ما يلي ضمن علامة التبويب الحاويات (:
    • علامة تبويب الإعدادات:
      • المراجع
        • الذاكرة : 2 غيبيبايت
    • علامة التبويب "المتغيرات والأسرار":
      • متغيّرات البيئة، أضِف المتغيّرات التالية بالنقر على الزر + إضافة متغيّر:
        • أضِف الاسم: GOOGLE_CLOUD_PROJECT والقيمة: <YOUR_PROJECT_ID>
        • أضِف الاسم: BOOK_PROVIDER_URL، واضبط القيمة على عنوان URL الخاص بدالة موفّر الكتب، ويمكنك تحديدها باستخدام الأمر التالي في نافذة الأوامر:
          gcloud config set project $(cat ~/project_id.txt)
          gcloud run services describe book-provider \
              --region=us-central1 \
              --project=$PROJECT_ID \
              --format="value(status.url)"
          
      • ضمن قسم الأسرار المعروضة كمتغيّرات بيئية، أضِف الأسرار التالية بالنقر على الزر + الإشارة إلى سر:
        • أضِف الاسم: DB_USER، والمفتاح السرّي: اختَر db-user، والإصدار:latest
        • أضِف الاسم: DB_PASS، والمفتاح السرّي: اختَر db-pass، والإصدار:latest
        • أضِف الاسم: DB_NAME، والمفتاح السرّي: اختَر db-name، والإصدار:latest

ضبط المفتاح السرّي

اترك القيم الأخرى على الإعدادات التلقائية.

👉 انقر على إنشاء.

ستنشر Cloud Run خدمتك.

بعد نشر الخدمة، إذا لم تكن في صفحة التفاصيل، انقر على اسم الخدمة للانتقال إلى صفحة التفاصيل. يمكنك العثور على عنوان URL الذي تم نشره في أعلى الصفحة.

عنوان URL

👉 في واجهة التطبيق، انقر على 7 للسنة، واختَر Mathematics كموضوع، وأدخِل Algebra في حقل "طلب الإضافة".

👉 انقر على إنشاء خطة. سيوفّر ذلك للوكيل السياق اللازم لإنشاء خطة درس مخصّصة.

تهانينا! لقد أنشأت بنجاح خطة تدريس باستخدام وكيل الذكاء الاصطناعي الفعّال. ويوضّح ذلك إمكانية أن تقلّل هذه الأدوات عبء العمل بشكل كبير وتسهّل المهام، ما يؤدي في النهاية إلى تحسين الكفاءة وتسهيل حياة المعلّمين.

9- الأنظمة المتعددة الوكلاء

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

كما ذكرنا سابقًا، بدلاً من الاعتماد على وكيل واحد للتعامل مع كل شيء، يتيح لنا نظام الوكلاء المتعدّدين تقسيم عبء العمل إلى مهام أصغر ومتخصّصة، ويتولّى كل وكيل مهمة محدّدة. يقدّم هذا الأسلوب العديد من المزايا الرئيسية:

التصميم المعياري وسهولة الصيانة: بدلاً من إنشاء وكيل واحد ينفّذ كل المهام، يمكنك إنشاء وكلاء أصغر حجمًا ومتخصّصين بمسؤوليات محدّدة جيدًا. وتسهّل هذه الوحداتية فهم النظام وصيانته وتصحيح الأخطاء فيه. عند حدوث مشكلة، يمكنك حصرها في وكيل معيّن، بدلاً من الاضطرار إلى البحث في قاعدة رموز برمجية ضخمة.

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

تخصّص الفريق: فكِّر في الأمر على النحو التالي: لن تطلب من مهندس واحد إنشاء تطبيق كامل من البداية. بدلاً من ذلك، يمكنك تكوين فريق من المتخصّصين، كلّ منهم لديه خبرة في مجال معيّن. وبالمثل، يتيح لك نظام الوكلاء المتعددين الاستفادة من نقاط القوة في نماذج اللغة الكبيرة والأدوات المختلفة، وتعيينها لوكلاء الأنسب لأداء مهام محددة.

التطوير المتوازي: يمكن لفِرق مختلفة العمل على وكلاء مختلفين في الوقت نفسه، ما يؤدي إلى تسريع عملية التطوير. بما أنّ الوكلاء مستقلون، من غير المرجّح أن تؤثّر التغييرات التي يتم إجراؤها على أحد الوكلاء في الوكلاء الآخرين.

البنية المستندة إلى الأحداث

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

تشترك البرامج في أنواع أحداث معيّنة (مثل تم إنشاء خطة تدريس"، "تم إنشاء مهمة"). عند وقوع حدث، يتم إشعار العملاء المعنيين ويمكنهم التفاعل وفقًا لذلك. يساعد هذا الفصل على تعزيز المرونة وقابلية التوسّع والاستجابة في الوقت الفعلي.

نظرة عامة

الآن، لبدء العملية، نحتاج إلى طريقة لبث هذه الأحداث. لإجراء ذلك، سنعدّ موضوعًا على Pub/Sub. لنبدأ بإنشاء موضوع باسم خطة.

👉 انتقِل إلى Google Cloud Console pub/sub.

👉 انقر على الزر إنشاء موضوع.

👉 اضبط الموضوع باستخدام رقم التعريف/الاسم plan وأزِل العلامة من المربّع Add a default subscription، واترك بقية الإعدادات كما هي تلقائيًا، ثم انقر على إنشاء.

ستتم إعادة تحميل صفحة Pub/Sub، ومن المفترض أن يظهر لك الآن الموضوع الذي تم إنشاؤه حديثًا في الجدول. إنشاء موضوع

الآن، لنُدمج وظيفة نشر أحداث Pub/Sub في وكيل التخطيط. سنضيف أداة جديدة تُرسل حدث "خطة" إلى موضوع Pub/Sub الذي أنشأناه للتو. سيشير هذا الحدث إلى البرامج الأخرى في النظام (مثل البرامج في بوابة الطالب) بأنّه يتوفّر خطة تدريس جديدة.

👉ارجع إلى "أداة تعديل رموز Cloud" وافتح الملف app.py الموجود في المجلد planner. سنضيف دالة تنشر الحدث. استبدال:

##ADD SEND PLAN EVENT FUNCTION HERE

باستخدام الرمز التالي

def send_plan_event(teaching_plan:str):
    """
    Send the teaching event to the topic called plan
    
    Args:
        teaching_plan: teaching plan
    """
    publisher = pubsub_v1.PublisherClient()
    print(f"-------------> Sending event to topic plan: {teaching_plan}")
    topic_path = publisher.topic_path(PROJECT_ID, "plan")

    message_data = {"teaching_plan": teaching_plan} 
    data = json.dumps(message_data).encode("utf-8") 

    future = publisher.publish(topic_path, data)

    return f"Published message ID: {future.result()}"

  • send_plan_event: تأخذ هذه الدالة خطة التدريس التي تم إنشاؤها كمدخل، وتنشئ عميل ناشر Pub/Sub، وتنشئ مسار الموضوع، وتحوّل خطة التدريس إلى سلسلة JSON، وتنشر الرسالة إلى الموضوع.

في ملف app.py نفسه

👉عدِّل الطلب لتوجيه الوكيل بإرسال حدث خطة التدريس إلى موضوع Pub/Sub بعد إنشاء خطة التدريس. *استبدال

### ADD send_plan_event CALL

مع ما يلي:

send_plan_event(teaching_plan)

من خلال إضافة أداة send_plan_event وتعديل الطلب، مكّنّا وكيل المخطّط من نشر الأحداث في Pub/Sub، ما يسمح لمكوّنات أخرى من نظامنا بالتفاعل مع إنشاء خطط تدريس جديدة. وسنحصل الآن على نظام متعدد الوكلاء يعمل في الأقسام التالية.

10. تعزيز قدرات الطلاب من خلال الاختبارات عند الطلب

تخيَّل بيئة تعليمية يمكن للطلاب فيها الوصول إلى عدد لا نهائي من الاختبارات القصيرة المصمَّمة خصيصًا لخططهم التعليمية. تقدّم هذه الاختبارات القصيرة ملاحظات فورية، بما في ذلك الإجابات والتفسيرات، ما يعزّز فهمهم للمادة الدراسية بشكل أفضل. هذه هي الإمكانات التي نسعى إلى إتاحتها من خلال بوابة الاختبارات المستندة إلى الذكاء الاصطناعي.

لتحقيق هذه الرؤية، سننشئ مكوّنًا لإنشاء الاختبارات يمكنه إنشاء أسئلة بخيارات متعدّدة استنادًا إلى محتوى خطة التدريس.

نظرة عامة

👉 في لوحة Explorer في Cloud Code Editor، انتقِل إلى المجلد portal. افتح نسخة ملف quiz.py والصِق الرمز التالي في نهاية الملف.

def generate_quiz_question(file_name: str, difficulty: str, region:str ):
    """Generates a single multiple-choice quiz question using the LLM.
   
    ```json
    {
      "question": "The question itself",
      "options": ["Option A", "Option B", "Option C", "Option D"],
      "answer": "The correct answer letter (A, B, C, or D)"
    }
    ```
    """

    print(f"region: {region}")
    # Connect to resourse needed from Google Cloud
    llm = VertexAI(model_name="gemini-2.5-flash-preview-04-17", location=region)


    plan=None
    #load the file using file_name and read content into string call plan
    with open(file_name, 'r') as f:
        plan = f.read()

    parser = JsonOutputParser(pydantic_object=QuizQuestion)


    instruction = f"You'll provide one question with difficulty level of {difficulty}, 4 options as multiple choices and provide the anwsers, the quiz needs to be related to the teaching plan {plan}"

    prompt = PromptTemplate(
        template="Generates a single multiple-choice quiz question\n {format_instructions}\n  {instruction}\n",
        input_variables=["instruction"],
        partial_variables={"format_instructions": parser.get_format_instructions()},
    )
    
    chain = prompt | llm | parser
    response = chain.invoke({"instruction": instruction})

    print(f"{response}")
    return  response


في الوكيل، يتم إنشاء محلّل مخرجات JSON مصمَّم خصيصًا لفهم مخرجات النموذج اللغوي الكبير وتنظيمها. يستخدم هذا النموذج نموذج QuizQuestion الذي حدّدناه سابقًا لضمان توافق الناتج الذي تم تحليله مع التنسيق الصحيح (السؤال والخيارات والإجابة).

👉 في الوحدة الطرفية، نفِّذ الأوامر التالية لإعداد بيئة افتراضية وتثبيت التبعيات وبدء تشغيل الوكيل:

gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py

👉 من قائمة "معاينة الويب" في أعلى يسار الصفحة، اختَر المعاينة على المنفذ 8080. سيفتح Cloud Shell علامة تبويب أو نافذة متصفّح جديدة تتضمّن معاينة الويب لتطبيقك.

👉 في تطبيق الويب، انقر على رابط "الاختبارات"، إما في شريط التنقّل العلوي أو من البطاقة في صفحة الفهرس. من المفترض أن تظهر ثلاثة اختبارات تم إنشاؤها عشوائيًا للطالب. تستند هذه الاختبارات إلى خطة التدريس وتوضّح إمكانات نظام إنشاء الاختبارات المستند إلى الذكاء الاصطناعي.

اختبارات

👉لإيقاف العملية الجارية محليًا، اضغط على Ctrl+C في نافذة الوحدة الطرفية.

‫Gemini 2 يفكّر في التفسيرات

حسنًا، لدينا الآن اختبارات، وهذا أمر رائع. ولكن ماذا لو أخطأ الطلاب في الإجابة؟ هذا هو المكان الذي يحدث فيه التعلّم الحقيقي، أليس كذلك؟ إذا تمكّنا من شرح سبب عدم صحة إجابته وكيفية الوصول إلى الإجابة الصحيحة، من المرجّح أن يتذكّرها. بالإضافة إلى ذلك، يساعد في إزالة أي التباس وتعزيز ثقتهم بأنفسهم.

لهذا السبب، سنستعين بأقوى الأدوات، أي نموذج "التفكير" في Gemini 2. يمكنك اعتبار ذلك بمثابة منح الذكاء الاصطناعي وقتًا إضافيًا للتفكير في الأمور قبل تقديم التفسير. ويتيح ذلك تقديم ملاحظات أكثر تفصيلاً وأفضل.

نريد معرفة ما إذا كان بإمكانه مساعدة الطلاب من خلال تقديم المساعدة والإجابة عن الأسئلة وتقديم شرح مفصّل. لاختبارها، سنبدأ بموضوع صعب للغاية، وهو التفاضل والتكامل.

نظرة عامة

👉أولاً، انتقِل إلى "أداة تعديل الرموز البرمجية على السحابة الإلكترونية" (Cloud Code Editor) في answer.py داخل المجلد portal. استبدال رمز الدالة التالي

def answer_thinking(question, options, user_response, answer, region):
    return ""

مع مقتطف الرمز التالي:

def answer_thinking(question, options, user_response, answer, region):
    try:
        llm = VertexAI(model_name="gemini-2.0-flash-001",location=region)
        
        input_msg = HumanMessage(content=[f"Here the question{question}, here are the available options {options}, this student's answer {user_response}, whereas the correct answer is {answer}"])
        prompt_template = ChatPromptTemplate.from_messages(
            [
                SystemMessage(
                    content=(
                        "You are a helpful teacher trying to teach the student on question, you were given the question and a set of multiple choices "
                        "what's the correct answer. use friendly tone"
                    )
                ),
                input_msg,
            ]
        )

        prompt = prompt_template.format()
        
        response = llm.invoke(prompt)
        print(f"response: {response}")

        return response
    except Exception as e:
        print(f"Error sending message to chatbot: {e}") # Log this error too!
        return f"Unable to process your request at this time. Due to the following reason: {str(e)}"



if __name__ == "__main__":
    question = "Evaluate the limit: lim (x→0) [(sin(5x) - 5x) / x^3]"
    options = ["A) -125/6", "B) -5/3 ", "C) -25/3", "D) -5/6"]
    user_response = "B"
    answer = "A"
    region = "us-central1"
    result = answer_thinking(question, options, user_response, answer, region)

هذا تطبيق بسيط جدًا من Langchain، حيث يتم فيه تهيئة نموذج Gemini 2 Flash، ونطلب منه أن يتصرف كمدرّس مفيد ويقدّم تفسيرات.

👉نفِّذ الأمر التالي في الوحدة الطرفية:

gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python answer.py

من المفترَض أن تظهر لك نتيجة مشابهة للمثال المقدَّم في التعليمات الأصلية. قد لا يقدّم النموذج الحالي شرحًا وافيًا.

Okay, I see the question and the choices. The question is to evaluate the limit:

lim (x0) [(sin(5x) - 5x) / x^3]

You chose option B, which is -5/3, but the correct answer is A, which is -125/6.

It looks like you might have missed a step or made a small error in your calculations. This type of limit often involves using L'Hôpital's Rule or Taylor series expansion. Since we have the form 0/0, L'Hôpital's Rule is a good way to go! You need to apply it multiple times. Alternatively, you can use the Taylor series expansion of sin(x) which is:
sin(x) = x - x^3/3! + x^5/5! - ...
So, sin(5x) = 5x - (5x)^3/3! + (5x)^5/5! - ...
Then,  (sin(5x) - 5x) = - (5x)^3/3! + (5x)^5/5! - ...
Finally, (sin(5x) - 5x) / x^3 = - 5^3/3! + (5^5 * x^2)/5! - ...
Taking the limit as x approaches 0, we get -125/6.

Keep practicing, you'll get there!

👉 في الملف answer.py، استبدِل

model_name من gemini-2.0-flash-001 إلى gemini-2.0-flash-thinking-exp-01-21 في الدالة answer_thinking

يؤدي ذلك إلى تغيير النموذج اللغوي الكبير إلى نموذج آخر يقدّم أداءً أفضل في الاستدلال. سيساعد ذلك النموذج في تقديم تفسيرات أفضل.

👉 شغِّل النص البرمجي answer.py مرة أخرى لاختبار نموذج التفكير الجديد:

gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python answer.py

في ما يلي مثال على الردّ من النموذج المُفكّر الذي يتضمّن تفاصيل أكثر بكثير، ويقدّم شرحًا مفصّلاً خطوة بخطوة لطريقة حلّ مسألة حساب التفاضل والتكامل. ويسلّط ذلك الضوء على قوة نماذج "التفكير" في إنشاء تفسيرات عالية الجودة. من المفترَض أن تظهر لك نتيجة مثل هذه:

Hey there! Let's take a look at this limit problem together. You were asked to evaluate:

lim (x0) [(sin(5x) - 5x) / x^3]

and you picked option B, -5/3, but the correct answer is actually A, -125/6. Let's figure out why!

It's a tricky one because if we directly substitute x=0, we get (sin(0) - 0) / 0^3 = (0 - 0) / 0 = 0/0, which is an indeterminate form. This tells us we need to use a more advanced technique like L'Hopital's Rule or Taylor series expansion.

Let's use the Taylor series expansion for sin(y) around y=0. Do you remember it?  It looks like this:

sin(y) = y - y^3/3! + y^5/5! - ...
where 3! (3 factorial) is 3 × 2 × 1 = 6, 5! is 5 × 4 × 3 × 2 × 1 = 120, and so on.

In our problem, we have sin(5x), so we can substitute y = 5x into the Taylor series:

sin(5x) = (5x) - (5x)^3/3! + (5x)^5/5! - ...
sin(5x) = 5x - (125x^3)/6 + (3125x^5)/120 - ...

Now let's plug this back into our limit expression:

[(sin(5x) - 5x) / x^3] =  [ (5x - (125x^3)/6 + (3125x^5)/120 - ...) - 5x ] / x^3
Notice that the '5x' and '-5x' cancel out!  So we are left with:
= [ - (125x^3)/6 + (3125x^5)/120 - ... ] / x^3
Now, we can divide every term in the numerator by x^3:
= -125/6 + (3125x^2)/120 - ...

Finally, let's take the limit as x approaches 0.  As x gets closer and closer to zero, terms with x^2 and higher powers will become very, very small and approach zero.  So, we are left with:
lim (x0) [ -125/6 + (3125x^2)/120 - ... ] = -125/6

Therefore, the correct answer is indeed **A) -125/6**.

It seems like your answer B, -5/3, might have come from perhaps missing a factor somewhere during calculation or maybe using an incorrect simplification. Double-check your steps when you were trying to solve it!

Don't worry, these limit problems can be a bit tricky sometimes! Keep practicing and you'll get the hang of it.  Let me know if you want to go through another similar example or if you have any more questions! 😊


Now that we have confirmed it works, let's use the portal.

👉أزِل رمز الاختبار التالي من answer.py:

if __name__ == "__main__":
    question = "Evaluate the limit: lim (x→0) [(sin(5x) - 5x) / x^3]"
    options = ["A) -125/6", "B) -5/3 ", "C) -25/3", "D) -5/6"]
    user_response = "B"
    answer = "A"
    region = "us-central1"
    result = answer_thinking(question, options, user_response, answer, region)

👉نفِّذ الأوامر التالية في الوحدة الطرفية لإعداد بيئة افتراضية وتثبيت التبعيات وبدء تشغيل الوكيل:

gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python app.py

👉 من قائمة "معاينة الويب" في أعلى يسار الصفحة، اختَر المعاينة على المنفذ 8080. سيفتح Cloud Shell علامة تبويب أو نافذة متصفّح جديدة تتضمّن معاينة الويب لتطبيقك.

👉 في تطبيق الويب، انقر على رابط "الاختبارات"، إما في شريط التنقّل العلوي أو من البطاقة في صفحة الفهرس.

👉 أجب عن جميع الاختبارات القصيرة وتأكَّد من الإجابة عن سؤال واحد على الأقل بشكل خاطئ، ثم انقر على إرسال.

التفكير في الإجابات

بدلاً من التحديق في الشاشة أثناء انتظار الرد، انتقِل إلى نافذة Cloud Editor. يمكنك مراقبة مستوى التقدّم وأي رسائل ناتجة أو رسائل خطأ تم إنشاؤها بواسطة الدالة في نافذة المحاكي. 😁

👉 في الوحدة الطرفية، أوقِف العملية التي يتم تنفيذها على الجهاز من خلال الضغط على Ctrl+C في الوحدة الطرفية.

11. اختياري: تنسيق الوكلاء باستخدام Eventarc

حتى الآن، كان بإمكان بوابة الطلاب إنشاء اختبارات استنادًا إلى مجموعة تلقائية من خطط التدريس، وهذا مفيد، ولكنّه يعني أنّ وكيل التخطيط ووكيل الاختبار في البوابة لا يتواصلان مع بعضهما البعض. هل تتذكر كيف أضفنا ميزة تتيح لوكيل التخطيط نشر خطط التدريس التي تم إنشاؤها حديثًا في موضوع Pub/Sub؟ حان الآن وقت ربط ذلك بموظف الدعم في البوابة.

نظرة عامة

نريد أن يتم تعديل محتوى الاختبارات في البوابة تلقائيًا كلما تم إنشاء خطة تدريس جديدة. لإجراء ذلك، سننشئ نقطة نهاية في البوابة يمكنها تلقّي هذه الخطط الجديدة.

👉 في لوحة Explorer في Cloud Code Editor، انتقِل إلى المجلد portal.

👉 افتح ملف app.py لتعديله. استبدِل سطر REPLACE ## REPLACE ME! NEW TEACHING PLAN بالرمز التالي:

@app.route('/new_teaching_plan', methods=['POST'])
def new_teaching_plan():
    try:
       
        # Get data from Pub/Sub message delivered via Eventarc
        envelope = request.get_json()
        if not envelope:
            return jsonify({'error': 'No Pub/Sub message received'}), 400

        if not isinstance(envelope, dict) or 'message' not in envelope:
            return jsonify({'error': 'Invalid Pub/Sub message format'}), 400

        pubsub_message = envelope['message']
        print(f"data: {pubsub_message['data']}")

        data = pubsub_message['data']
        data_str = base64.b64decode(data).decode('utf-8')
        data = json.loads(data_str)

        teaching_plan = data['teaching_plan']

        print(f"File content: {teaching_plan}")

        with open("teaching_plan.txt", "w") as f:
            f.write(teaching_plan)

        print(f"Teaching plan saved to local file: teaching_plan.txt")

        return jsonify({'message': 'File processed successfully'})


    except Exception as e:
        print(f"Error processing file: {e}")
        return jsonify({'error': 'Error processing file'}), 500

إعادة البناء والنشر على Cloud Run

عليك تعديل كل من وكيلَي "المخطّط" و"البوابة" وإعادة نشرهما على Cloud Run. يضمن ذلك حصولها على أحدث الرموز البرمجية وإعدادها للتواصل من خلال الأحداث.

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

👉أولاً، سنعيد إنشاء صورة وكيل المخطِّط ونحمّلها. في الوحدة الطرفية، نفِّذ الأمر التالي:

cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner

👉سننفّذ الخطوات نفسها، وننشئ صورة وكيل البوابة ونحمّلها:

cd ~/aidemy-bootstrap/portal/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal

👉 انتقِل إلى Artifact Registry، من المفترض أن تظهر لك صورة الحاوية aidemy-planner وصورة الحاوية aidemy-portal مُدرَجتَين ضمن agent-repository.

مستودع الحاويات

👉في الوحدة الطرفية، شغِّل هذا الأمر لتعديل صورة Cloud Run الخاصة بوكيل المخطّط:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-planner \
    --region=us-central1 \
    --image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner:latest

من المفترَض أن تظهر لك نتيجة مثل هذه:

OK Deploying... Done.                                                                                                                                                     
  OK Creating Revision...                                                                                                                                                 
  OK Routing traffic...                                                                                                                                                   
Done.                                                                                                                                                                     
Service [aidemy-planner] revision [aidemy-planner-xxxxx] has been deployed and is serving 100 percent of traffic.
Service URL: https://aidemy-planner-xxx.us-central1.run.app

دوِّن عنوان URL الخاص بالخدمة، فهذا هو الرابط المؤدي إلى وكيل المخطط الذي تم نشره. إذا كنت بحاجة إلى تحديد عنوان URL الخاص بخدمة وكيل المخطط في وقت لاحق، استخدِم الأمر التالي:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services describe aidemy-planner \
    --region=us-central1 \
    --format 'value(status.url)'

👉نفِّذ هذا الأمر لإنشاء مثيل Cloud Run لوكيل البوابة

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run deploy aidemy-portal \
  --image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal:latest \
  --region=us-central1 \
  --platform=managed \
  --allow-unauthenticated \
  --memory=2Gi \
  --cpu=2 \
  --set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID}

من المفترَض أن تظهر لك نتيجة مثل هذه:

Deploying container to Cloud Run service [aidemy-portal] in project [xxxx] region [us-central1]
OK Deploying new service... Done.                                                                                                                                         
  OK Creating Revision...                                                                                                                                                 
  OK Routing traffic...                                                                                                                                                   
  OK Setting IAM Policy...                                                                                                                                                
Done.                                                                                                                                                                     
Service [aidemy-portal] revision [aidemy-portal-xxxx] has been deployed and is serving 100 percent of traffic.
Service URL: https://aidemy-portal-xxxx.us-central1.run.app

دوِّن عنوان URL الخاص بالخدمة، فهذا هو الرابط المؤدي إلى بوابة الطلاب التي تم نشرها. إذا احتجت إلى تحديد عنوان URL الخاص بخدمة بوابة الطالب في وقت لاحق، استخدِم الأمر التالي:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services describe aidemy-portal \
    --region=us-central1 \
    --format 'value(status.url)'

إنشاء مشغّل Eventarc

ولكن السؤال المهم هو: كيف يتم إعلام نقطة النهاية هذه عندما تكون هناك خطة جديدة في انتظارها في موضوع Pub/Sub؟ هنا يأتي دور Eventarc لإنقاذ الموقف.

تعمل Eventarc كجسر، حيث تستمع إلى أحداث معيّنة (مثل وصول رسالة جديدة إلى موضوع Pub/Sub) وتؤدي تلقائيًا إجراءات استجابةً لذلك. في حالتنا، سيتم رصد نشر خطة تدريس جديدة، ثم إرسال إشارة إلى نقطة النهاية في البوابة الإلكترونية لإعلامها بأنّه قد حان وقت التعديل.

بفضل خدمة Eventarc التي تتولّى عملية التواصل المستند إلى الأحداث، يمكننا ربط وكيل التخطيط ووكيل البوابة بسلاسة، ما يؤدي إلى إنشاء نظام تعليمي ديناميكي ومتجاوب حقًا. يشبه الأمر وجود برنامج مراسلة ذكي يسلّم تلقائيًا أحدث خطط الدروس إلى المكان الصحيح.

👉في وحدة التحكّم، انتقِل إلى Eventarc.

👉انقر على الزر "+ إنشاء مشغّل".

ضبط المشغِّل (الأساسيات):

  • اسم المشغّل: plan-topic-trigger
  • نوع المشغّل: مصادر Google
  • مقدّم الأحداث: Cloud Pub/Sub
  • نوع الحدث: google.cloud.pubsub.topic.v1.messagePublished
  • موضوع Cloud Pub/Sub: اختَر projects/PROJECT_ID/topics/plan
  • المنطقة: us-central1
  • حساب الخدمة:
    • GRANT حساب الخدمة بالدور roles/iam.serviceAccountTokenCreator
    • استخدام القيمة التلقائية: حساب خدمة Compute التلقائي
  • وجهة الحدث: Cloud Run
  • خدمة Cloud Run: aidemy-portal
  • تجاهُل رسالة الخطأ: تم رفض إذن الوصول إلى "locations/me-central2" (أو قد يكون هذا المورد غير موجود).
  • مسار عنوان URL الخاص بالخدمة: /new_teaching_plan

👉 انقر على "إنشاء".

ستتم إعادة تحميل صفحة "المشغّلات" في Eventarc، ومن المفترض أن يظهر المشغّل الذي أنشأته حديثًا في الجدول.

الآن، يمكنك الوصول إلى وكيل المخطّط باستخدام عنوان URL الخاص بالخدمة لطلب خطة تدريس جديدة.

👉 شغِّل هذا الأمر في الوحدة الطرفية لتحديد عنوان URL الخاص بخدمة وكيل المخطط:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner

👉 انتقِل إلى عنوان URL الذي تم إخراجه، وجرِّب هذه المرة "السنة" 5 و"الموضوع" Science و"طلب الإضافة" atoms.

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

أخيرًا، يمكنك الوصول إلى بوابة الطالب باستخدام عنوان URL الخاص بالخدمة.

نفِّذ الأمر التالي في الوحدة الطرفية لتحديد عنوان URL لخدمة وكيل بوابة الطالب:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal

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

Aidemy-celebrate

تهانينا! لقد أنشأت بنجاح نظامًا متعدد الوكلاء على Google Cloud، مستفيدًا من بنية مستندة إلى الأحداث لتحسين قابلية التوسّع والمرونة. لقد وضعت أساسًا متينًا، ولكن لا يزال هناك المزيد لاستكشافه. للتعمّق أكثر في المزايا الحقيقية لهذه البنية، والتعرّف على إمكانات واجهة برمجة التطبيقات المتعددة الوسائط Live API من Gemini 2، ومعرفة كيفية تنفيذ التنسيق أحادي المسار باستخدام LangGraph، يمكنك الانتقال إلى الفصلَين التاليَين.

12. اختياري: ملخّصات صوتية باستخدام Gemini

يمكن لـ Gemini فهم المعلومات ومعالجتها من مصادر مختلفة، مثل النصوص والصور وحتى المقاطع الصوتية، ما يفتح آفاقًا جديدة تمامًا للتعلم وإنشاء المحتوى. تتيح قدرة Gemini على "الرؤية" و"الاستماع" و"القراءة" تجارب مستخدم إبداعية وجذابة.

بالإضافة إلى إنشاء صور أو نصوص، هناك خطوة مهمة أخرى في عملية التعلّم وهي التلخيص الفعّال وإعادة الصياغة. فكِّر في الأمر: كم مرة تذكرت كلمات أغنية جذابة بسهولة أكبر من شيء قرأته في كتاب مدرسي؟ يمكن أن يكون الصوت لا يُنسى! لهذا السبب، سنستفيد من إمكانات Gemini المتعدّدة الوسائط لإنشاء ملخّصات صوتية لخططنا التعليمية. سيتيح ذلك للطلاب طريقة سهلة وجذابة لمراجعة المواد الدراسية، ما قد يعزّز قدرتهم على تذكُّر المعلومات وفهمها من خلال التعلّم السمعي.

نظرة عامة على Live API

نحتاج إلى مكان لتخزين الملفات الصوتية التي تم إنشاؤها. توفّر Cloud Storage حلاً قابلاً للتوسّع وموثوقًا.

👉انتقِل إلى مساحة التخزين في وحدة التحكّم. انقر على "الحِزم" في القائمة اليمنى. انقر على الزر "+ إنشاء" في أعلى الصفحة.

👉اضبط إعدادات المجموعة الجديدة:

  • اسم الحزمة: aidemy-recap-UNIQUE_NAME.
    • ملاحظة مهمة: احرص على تحديد اسم حزمة فريد يبدأ بـ aidemy-recap-. هذه البادئة الفريدة ضرورية لتجنُّب تعارض الأسماء عند إنشاء حزمة Cloud Storage.
  • المنطقة: us-central1
  • فئة التخزين: "عادية" فئة Standard مناسبة للبيانات التي يتم الوصول إليها بشكل متكرر.
  • التحكّم في الوصول: اترِك خيار التحكّم في الوصول التلقائي "موحّد" محدّدًا. يوفّر ذلك إمكانية التحكّم في الوصول بشكل متّسق على مستوى الحزمة.
  • الخيارات المتقدّمة: تكون الإعدادات التلقائية كافية عادةً في ورشة العمل هذه.

انقر على الزر إنشاء لإنشاء الحزمة.

  • قد تظهر لك نافذة منبثقة بشأن منع الوصول إلى الملفات بشكل علني. اترك المربّع "فرض منع الوصول العام إلى هذا الحزمة" محدّدًا وانقر على Confirm.

سيظهر لك الآن الحزمة التي أنشأتها حديثًا في قائمة "الحِزم". تذكَّر اسم الحزمة، ستحتاج إليه لاحقًا.

👉في وحدة التحكّم الطرفية في "محرّر Cloud Code"، شغِّل الأوامر التالية لمنح حساب الخدمة إذن الوصول إلى الحزمة:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
    --member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
    --role "roles/storage.objectViewer"

gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
    --member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
    --role "roles/storage.objectCreator"

👉في "محرّر رمز Cloud"، افتح audio.py داخل المجلد courses. ألصِق الرمز التالي في نهاية الملف:

config = LiveConnectConfig(
    response_modalities=["AUDIO"],
    speech_config=SpeechConfig(
        voice_config=VoiceConfig(
            prebuilt_voice_config=PrebuiltVoiceConfig(
                voice_name="Charon",
            )
        )
    ),
)

async def process_weeks(teaching_plan: str):
    region = "us-east5" #To workaround onRamp quota limits
    client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
    
    clientAudio = genai.Client(vertexai=True, project=PROJECT_ID, location="us-central1")
    async with clientAudio.aio.live.connect(
        model=MODEL_ID,
        config=config,
    ) as session:
        for week in range(1, 4):  
            response = client.models.generate_content(
                model="gemini-2.0-flash-001",
                contents=f"Given the following teaching plan: {teaching_plan}, Extrace content plan for week {week}. And return just the plan, nothingh else  " # Clarified prompt
            )

            prompt = f"""
                Assume you are the instructor.  
                Prepare a concise and engaging recap of the key concepts and topics covered. 
                This recap should be suitable for generating a short audio summary for students. 
                Focus on the most important learnings and takeaways, and frame it as a direct address to the students.  
                Avoid overly formal language and aim for a conversational tone, tell a few jokes. 
                
                Teaching plan: {response.text} """
            print(f"prompt --->{prompt}")

            await session.send(input=prompt, end_of_turn=True)
            with open(f"temp_audio_week_{week}.raw", "wb") as temp_file:
                async for message in session.receive():
                    if message.server_content.model_turn:
                        for part in message.server_content.model_turn.parts:
                            if part.inline_data:
                                temp_file.write(part.inline_data.data)
                            
            data, samplerate = sf.read(f"temp_audio_week_{week}.raw", channels=1, samplerate=24000, subtype='PCM_16', format='RAW')
            sf.write(f"course-week-{week}.wav", data, samplerate)
        
            storage_client = storage.Client()
            bucket = storage_client.bucket(BUCKET_NAME)
            blob = bucket.blob(f"course-week-{week}.wav")  # Or give it a more descriptive name
            blob.upload_from_filename(f"course-week-{week}.wav")
            print(f"Audio saved to GCS: gs://{BUCKET_NAME}/course-week-{week}.wav")
    await session.close()

 
def breakup_sessions(teaching_plan: str):
    asyncio.run(process_weeks(teaching_plan))
  • اتصال البث: أولاً، يتم إنشاء اتصال دائم بنقطة نهاية Live API. وعلى عكس طلب البيانات العادي من واجهة برمجة التطبيقات الذي يتم فيه إرسال طلب وتلقّي ردّ، يظل هذا الاتصال مفتوحًا لتبادل البيانات بشكل مستمر.
  • الإعدادات المتعدّدة الوسائط: استخدِم الإعدادات لتحديد نوع الإخراج الذي تريده (في هذه الحالة، الصوت)، ويمكنك حتى تحديد المَعلمات التي تريد استخدامها (مثل اختيار الصوت وتشفير الصوت).
  • المعالجة غير المتزامنة: تعمل واجهة برمجة التطبيقات هذه بشكل غير متزامن، ما يعني أنّها لا تحظر سلسلة التعليمات الرئيسية أثناء انتظار اكتمال إنشاء الصوت. ومن خلال معالجة البيانات في الوقت الفعلي وإرسال الناتج على شكل أجزاء، يقدّم تجربة شبه فورية.

والسؤال الرئيسي الآن هو: متى يجب تنفيذ عملية إنشاء الصوت هذه؟ نريد أن تكون الملخّصات الصوتية متاحة فور إنشاء خطة تدريس جديدة. بما أنّنا سبق أن نفّذنا بنية مستندة إلى الأحداث من خلال نشر خطة التدريس في موضوع Pub/Sub، يمكننا ببساطة الاشتراك في هذا الموضوع.

ومع ذلك، لا ننشئ خطط تدريس جديدة كثيرًا. لن يكون من الفعّال أن يكون هناك وكيل يعمل باستمرار وينتظر خططًا جديدة، ولهذا السبب من المنطقي تمامًا نشر منطق إنشاء المحتوى الصوتي هذا كدالة Cloud Run.

من خلال نشرها كدالة، تظل غير نشطة إلى أن يتم نشر رسالة جديدة في موضوع النشر/الاشتراك. وعند حدوث ذلك، يتم تشغيل الدالة تلقائيًا، ما يؤدي إلى إنشاء الملخّصات الصوتية وتخزينها في الحزمة.

👉ضمن المجلد courses في الملف main.py، يحدّد هذا الملف دالة Cloud Run التي سيتم تشغيلها عندما تتوفّر خطة تعليمية جديدة. يتلقّى الخطة ويبدأ في إنشاء الملخّص الصوتي. أضِف مقتطف الرمز التالي إلى نهاية الملف.

@functions_framework.cloud_event
def process_teaching_plan(cloud_event):
    print(f"CloudEvent received: {cloud_event.data}")
    time.sleep(60)
    try:
        if isinstance(cloud_event.data.get('message', {}).get('data'), str):  # Check for base64 encoding
            data = json.loads(base64.b64decode(cloud_event.data['message']['data']).decode('utf-8'))
            teaching_plan = data.get('teaching_plan') # Get the teaching plan
        elif 'teaching_plan' in cloud_event.data: # No base64
            teaching_plan = cloud_event.data["teaching_plan"]
        else:
            raise KeyError("teaching_plan not found") # Handle error explicitly

        #Load the teaching_plan as string and from cloud event, call audio breakup_sessions
        breakup_sessions(teaching_plan)

        return "Teaching plan processed successfully", 200

    except (json.JSONDecodeError, AttributeError, KeyError) as e:
        print(f"Error decoding CloudEvent data: {e} - Data: {cloud_event.data}")
        return "Error processing event", 500

    except Exception as e:
        print(f"Error processing teaching plan: {e}")
        return "Error processing teaching plan", 500

‎@functions_framework.cloud_event: يضع هذا العنصر الزخرفي علامة على الدالة باعتبارها دالة Cloud Run سيتم تشغيلها بواسطة CloudEvents.

الاختبار على الجهاز

👉سننفّذ هذا الإجراء في بيئة افتراضية ونثبّت مكتبات Python اللازمة لدالة Cloud Run.

cd ~/aidemy-bootstrap/courses
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
python -m venv env
source env/bin/activate
pip install -r requirements.txt

👉يتيح لنا محاكي Cloud Run Function اختبار وظيفتنا محليًا قبل نشرها على Google Cloud. ابدأ تشغيل محاكي على جهازك من خلال تنفيذ ما يلي:

functions-framework --target process_teaching_plan --signature-type=cloudevent --source main.py

👉أثناء تشغيل المحاكي، يمكنك إرسال أحداث CloudEvents تجريبية إلى المحاكي لمحاكاة نشر خطة تعليمية جديدة. في نافذة طرفية جديدة:

طرفان

👉تشغيل:

  curl -X POST \
  http://localhost:8080/ \
  -H "Content-Type: application/json" \
  -H "ce-id: event-id-01" \
  -H "ce-source: planner-agent" \
  -H "ce-specversion: 1.0" \
  -H "ce-type: google.cloud.pubsub.topic.v1.messagePublished" \
  -d '{
    "message": {
      "data": "eyJ0ZWFjaGluZ19wbGFuIjogIldlZWsgMTogMkQgU2hhcGVzIGFuZCBBbmdsZXMgLSBEYXkgMTogUmV2aWV3IG9mIGJhc2ljIDJEIHNoYXBlcyAoc3F1YXJlcywgcmVjdGFuZ2xlcywgdHJpYW5nbGVzLCBjaXJjbGVzKS4gRGF5IDI6IEV4cGxvcmluZyBkaWZmZXJlbnQgdHlwZXMgb2YgdHJpYW5nbGVzIChlcXVpbGF0ZXJhbCwgaXNvc2NlbGVzLCBzY2FsZW5lLCByaWdodC1hbmdsZWQpLiBEYXkgMzogRXhwbG9yaW5nIHF1YWRyaWxhdGVyYWxzIChzcXVhcmUsIHJlY3RhbmdsZSwgcGFyYWxsZWxvZ3JhbSwgcmhvbWJ1cywgdHJhcGV6aXVtKS4gRGF5IDQ6IEludHJvZHVjdGlvbiB0byBhbmdsZXM6IHJpZ2h0IGFuZ2xlcywgYWN1dGUgYW5nbGVzLCBhbmQgb2J0dXNlIGFuZ2xlcy4gRGF5IDU6IE1lYXN1cmluZyBhbmdsZXMgdXNpbmcgYSBwcm90cmFjdG9yLiBXZWVrIDI6IDNEIFNoYXBlcyBhbmQgU3ltbWV0cnkgLSBEYXkgNjogSW50cm9kdWN0aW9uIHRvIDNEIHNoYXBlczogY3ViZXMsIGN1Ym9pZHMsIHNwaGVyZXMsIGN5bGluZGVycywgY29uZXMsIGFuZCBweXJhbWlkcy4gRGF5IDc6IERlc2NyaWJpbmcgM0Qgc2hhcGVzIHVzaW5nIGZhY2VzLCBlZGdlcywgYW5kIHZlcnRpY2VzLiBEYXkgODogUmVsYXRpbmcgMkQgc2hhcGVzIHRvIDNEIHNoYXBlcy4gRGF5IDk6IElkZW50aWZ5aW5nIGxpbmVzIG9mIHN5bW1ldHJ5IGluIDJEIHNoYXBlcy4gRGF5IDEwOiBDb21wbGV0aW5nIHN5bW1ldHJpY2FsIGZpZ3VyZXMuIFdlZWsgMzogUG9zaXRpb24sIERpcmVjdGlvbiwgYW5kIFByb2JsZW0gU29sdmluZyAtIERheSAxMTogRGVzY3JpYmluZyBwb3NpdGlvbiB1c2luZyBjb29yZGluYXRlcyBpbiB0aGUgZmlyc3QgcXVhZHJhbnQuIERheSAxMjogUGxvdHRpbmcgY29vcmRpbmF0ZXMgdG8gZHJhdyBzaGFwZXMuIERheSAxMzogVW5kZXJzdGFuZGluZyB0cmFuc2xhdGlvbiAoc2xpZGluZyBhIHNoYXBlKS4gRGF5IDE0OiBVbmRlcnN0YW5kaW5nIHJlZmxlY3Rpb24gKGZsaXBwaW5nIGEgc2hhcGUpLiBEYXkgMTU6IFByb2JsZW0tc29sdmluZyBhY3Rpdml0aWVzIGludm9sdmluZyBwZXJpbWV0ZXIsIGFyZWEsIGFuZCBtaXNzaW5nIGFuZ2xlcy4ifQ=="
    }
  }'

بدلاً من التحديق في الشاشة بلا هدف أثناء انتظار الردّ، انتقِل إلى نافذة Cloud Shell الأخرى. يمكنك مراقبة مستوى التقدّم وأي رسائل ناتجة أو رسائل خطأ تم إنشاؤها بواسطة الدالة في نافذة المحاكي. 😁

في الوحدة الطرفية الثانية، من المفترض أن تظهر لك القيمة OK.

👉للتحقّق من البيانات في الحزمة، انتقِل إلى Cloud Storage واختَر علامة التبويب "الحزمة"، ثم انقر على aidemy-recap-UNIQUE_NAME.

الحزمة

👉في الوحدة الطرفية التي يتم تشغيل المحاكي فيها، اكتب ctrl+c للخروج. وأغلِق الوحدة الطرفية الثانية. وأغلِق الوحدة الطرفية الثانية، ثم شغِّل deactivate للخروج من البيئة الافتراضية.

deactivate

النشر على Google Cloud

نظرة عامة على عملية النشر 👉بعد الاختبار على الجهاز المحلي، حان الوقت لنشر وكيل الدورة التدريبية على Google Cloud. في الوحدة الطرفية، شغِّل الأوامر التالية:

cd ~/aidemy-bootstrap/courses
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud functions deploy courses-agent \
  --region=us-central1 \
  --gen2 \
  --source=. \
  --runtime=python312 \
  --trigger-topic=plan \
  --entry-point=process_teaching_plan \
  --set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME

تحقَّق من عملية النشر من خلال الانتقال إلى Cloud Run في Google Cloud Console.من المفترض أن تظهر لك خدمة جديدة باسم courses-agent.

قائمة Cloud Run

للتحقّق من إعدادات المشغّل، انقر على خدمة "الدورات التدريبية-الوكيل" لعرض تفاصيلها. انتقِل إلى علامة التبويب "المشغّلات".

من المفترض أن يظهر لك مشغّل تم إعداده للاستماع إلى الرسائل المنشورة في موضوع الخطة.

مشغّل Cloud Run

أخيرًا، لنشاهد عملية التنفيذ الشامل.

👉علينا ضبط إعدادات وكيل البوابة لكي يعرف مكان العثور على ملفات الصوت التي تم إنشاؤها. في الوحدة الطرفية، شغِّل الأمر التالي:

export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-portal \
    --region=us-central1 \
    --set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME

👉جرِّب إنشاء خطة تدريس جديدة باستخدام صفحة الويب الخاصة بأداة التخطيط. قد يستغرق بدء التشغيل بضع دقائق، لذا لا داعي للقلق، فهذه خدمة بلا خادم.

للوصول إلى وكيل التخطيط، احصل على عنوان URL الخاص بالخدمة من خلال تنفيذ ما يلي في نافذة الأوامر:

gcloud run services list \
    --platform=managed \
    --region=us-central1 \
    --format='value(URL)' | grep planner

بعد إنشاء الخطة الجديدة، انتظِر من دقيقتَين إلى 3 دقائق إلى أن يتم إنشاء الصوت، علمًا أنّ ذلك سيستغرق بضع دقائق إضافية بسبب قيود الفوترة في حساب المختبر هذا.

يمكنك مراقبة ما إذا كانت الدالة courses-agent قد تلقّت خطة التدريس من خلال التحقّق من علامة التبويب "المشغّلات" الخاصة بالدالة. أعِد تحميل الصفحة بشكلٍ دوري، ومن المفترض أن تلاحظ في النهاية أنّه تم استدعاء الدالة. إذا لم يتم استدعاء الدالة بعد مرور أكثر من دقيقتَين، يمكنك محاولة إنشاء خطة التدريس مرة أخرى. ومع ذلك، تجنَّب إنشاء الخطط بشكل متكرّر في تتابع سريع، لأنّ كل خطة يتم إنشاؤها سيستهلكها الوكيل ويعالجها بالتسلسل، ما قد يؤدي إلى تراكم المهام.

مراقبة المشغّل

👉انتقِل إلى البوابة وانقر على "الدورات التدريبية". من المفترض أن تظهر لك ثلاث بطاقات، تعرض كلّ منها ملخّصًا صوتيًا. للعثور على عنوان URL الخاص بوكيل البوابة، اتّبِع الخطوات التالية:

gcloud run services list \
    --platform=managed \
    --region=us-central1 \
    --format='value(URL)' | grep portal

انقر على "تشغيل" في كل دورة تدريبية للتأكّد من أنّ الملخّصات الصوتية تتوافق مع خطة التدريس التي أنشأتها للتو. دورات البوابة الإلكترونية

اخرج من البيئة الافتراضية.

deactivate

13. اختياري: التعاون المستند إلى الأدوار مع Gemini وDeepSeek

تُعدّ وجهات النظر المتعددة قيّمة للغاية، لا سيما عند إنشاء مهام جذابة ومدروسة. سننشئ الآن نظامًا متعدد الوكلاء يستفيد من نموذجين مختلفين بأدوار متميزة لإنشاء المهام: أحدهما يعزّز التعاون، والآخر يشجّع الدراسة الذاتية. سنستخدم بنية "اللقطة الواحدة"، حيث يتّبع سير العمل مسارًا ثابتًا.

أداة إنشاء المهام في Gemini

نبذة باستخدام Gemini سنبدأ بإعداد وظيفة Gemini لإنشاء مهام تركّز على التعاون. عدِّل الملف gemini.py الموجود في المجلد assignment.

👉ألصِق الرمز التالي في نهاية ملف gemini.py:

def gen_assignment_gemini(state):
    region=get_next_region()
    client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
    print(f"---------------gen_assignment_gemini")
    response = client.models.generate_content(
        model=MODEL_ID, contents=f"""
        You are an instructor 

        Develop engaging and practical assignments for each week, ensuring they align with the teaching plan's objectives and progressively build upon each other.  

        For each week, provide the following:

        * **Week [Number]:** A descriptive title for the assignment (e.g., "Data Exploration Project," "Model Building Exercise").
        * **Learning Objectives Assessed:** List the specific learning objectives from the teaching plan that this assignment assesses.
        * **Description:** A detailed description of the task, including any specific requirements or constraints.  Provide examples or scenarios if applicable.
        * **Deliverables:** Specify what students need to submit (e.g., code, report, presentation).
        * **Estimated Time Commitment:**  The approximate time students should dedicate to completing the assignment.
        * **Assessment Criteria:** Briefly outline how the assignment will be graded (e.g., correctness, completeness, clarity, creativity).

        The assignments should be a mix of individual and collaborative work where appropriate.  Consider different learning styles and provide opportunities for students to apply their knowledge creatively.

        Based on this teaching plan: {state["teaching_plan"]}
        """
    )

    print(f"---------------gen_assignment_gemini answer {response.text}")
    
    state["model_one_assignment"] = response.text
    
    return state


import unittest

class TestGenAssignmentGemini(unittest.TestCase):
    def test_gen_assignment_gemini(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assigmodel_one_assignmentnment": "", "final_assignment": ""}

        updated_state = gen_assignment_gemini(initial_state)

        self.assertIn("model_one_assignment", updated_state)
        self.assertIsNotNone(updated_state["model_one_assignment"])
        self.assertIsInstance(updated_state["model_one_assignment"], str)
        self.assertGreater(len(updated_state["model_one_assignment"]), 0)
        print(updated_state["model_one_assignment"])


if __name__ == '__main__':
    unittest.main()

تستخدم هذه الميزة نموذج Gemini لإنشاء المهام.

نحن جاهزون لاختبار "وكيل Gemini".

👉نفِّذ الأوامر التالية في الوحدة الطرفية لإعداد البيئة:

cd ~/aidemy-bootstrap/assignment
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
python -m venv env
source env/bin/activate
pip install -r requirements.txt

👉يمكنك تشغيلها لاختبارها:

python gemini.py

من المفترض أن يظهر لك في الناتج مهمة تتضمّن المزيد من العمل الجماعي. سيؤدي اختبار التأكيد في النهاية أيضًا إلى إخراج النتائج.

Here are some engaging and practical assignments for each week, designed to build progressively upon the teaching plan's objectives:

**Week 1: Exploring the World of 2D Shapes**

* **Learning Objectives Assessed:**
    * Identify and name basic 2D shapes (squares, rectangles, triangles, circles).
    * .....

* **Description:**
    * **Shape Scavenger Hunt:** Students will go on a scavenger hunt in their homes or neighborhoods, taking pictures of objects that represent different 2D shapes. They will then create a presentation or poster showcasing their findings, classifying each shape and labeling its properties (e.g., number of sides, angles, etc.). 
    * **Triangle Trivia:** Students will research and create a short quiz or presentation about different types of triangles, focusing on their properties and real-world examples. 
    * **Angle Exploration:** Students will use a protractor to measure various angles in their surroundings, such as corners of furniture, windows, or doors. They will record their measurements and create a chart categorizing the angles as right, acute, or obtuse. 
....

**Week 2: Delving into the World of 3D Shapes and Symmetry**

* **Learning Objectives Assessed:**
    * Identify and name basic 3D shapes.
    * ....

* **Description:**
    * **3D Shape Construction:** Students will work in groups to build 3D shapes using construction paper, cardboard, or other materials. They will then create a presentation showcasing their creations, describing the number of faces, edges, and vertices for each shape. 
    * **Symmetry Exploration:** Students will investigate the concept of symmetry by creating a visual representation of various symmetrical objects (e.g., butterflies, leaves, snowflakes) using drawing or digital tools. They will identify the lines of symmetry and explain their findings. 
    * **Symmetry Puzzles:** Students will be given a half-image of a symmetrical figure and will be asked to complete the other half, demonstrating their understanding of symmetry. This can be done through drawing, cut-out activities, or digital tools.

**Week 3: Navigating Position, Direction, and Problem Solving**

* **Learning Objectives Assessed:**
    * Describe position using coordinates in the first quadrant.
    * ....

* **Description:**
    * **Coordinate Maze:** Students will create a maze using coordinates on a grid paper. They will then provide directions for navigating the maze using a combination of coordinate movements and translation/reflection instructions. 
    * **Shape Transformations:** Students will draw shapes on a grid paper and then apply transformations such as translation and reflection, recording the new coordinates of the transformed shapes. 
    * **Geometry Challenge:** Students will solve real-world problems involving perimeter, area, and angles. For example, they could be asked to calculate the perimeter of a room, the area of a garden, or the missing angle in a triangle. 
....

توقّف عند ctl+c، ثم نظِّف رمز الاختبار. أزِل الرمز التالي من gemini.py

import unittest

class TestGenAssignmentGemini(unittest.TestCase):
    def test_gen_assignment_gemini(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assigmodel_one_assignmentnment": "", "final_assignment": ""}

        updated_state = gen_assignment_gemini(initial_state)

        self.assertIn("model_one_assignment", updated_state)
        self.assertIsNotNone(updated_state["model_one_assignment"])
        self.assertIsInstance(updated_state["model_one_assignment"], str)
        self.assertGreater(len(updated_state["model_one_assignment"]), 0)
        print(updated_state["model_one_assignment"])


if __name__ == '__main__':
    unittest.main()

ضبط أداة إنشاء المهام في DeepSeek

على الرغم من أنّ منصات الذكاء الاصطناعي المستندة إلى السحابة الإلكترونية توفّر الراحة، يمكن أن يكون استضافة نماذج اللغات الكبيرة الذاتية أمرًا بالغ الأهمية لحماية خصوصية البيانات وضمان سيادة البيانات. سننشر أصغر نموذج DeepSeek (يحتوي على 1.5 مليار مَعلمة) على إحدى آلات Cloud Compute Engine الافتراضية. هناك طرق أخرى، مثل استضافة النموذج على منصة Vertex AI من Google أو استضافته على مثيل GKE، ولكن بما أنّ هذه مجرد ورشة عمل حول وكلاء الذكاء الاصطناعي، ولا أريد إطالة الجلسة، سنستخدم أبسط طريقة. إذا كنت مهتمًا وتريد استكشاف خيارات أخرى، يمكنك الاطّلاع على ملف deepseek-vertexai.py ضمن مجلد المهام، حيث يقدّم رمزًا برمجيًا نموذجيًا حول كيفية التفاعل مع النماذج التي تم نشرها على Vertex AI.

نظرة عامة حول Deepseek

👉نفِّذ الأمر التالي في الوحدة الطرفية لإنشاء منصة نماذج لغوية كبيرة (LLM) مستضافة ذاتيًا باسم Ollama:

cd ~/aidemy-bootstrap/assignment
gcloud config set project $(cat ~/project_id.txt)
gcloud compute instances create ollama-instance \
    --image-family=ubuntu-2204-lts \
    --image-project=ubuntu-os-cloud \
    --machine-type=e2-standard-4 \
    --zone=us-central1-a \
    --metadata-from-file startup-script=startup.sh \
    --boot-disk-size=50GB \
    --tags=ollama \
    --scopes=https://www.googleapis.com/auth/cloud-platform

للتحقّق من أنّ آلة Compute Engine الافتراضية تعمل، اتّبِع الخطوات التالية:

انتقِل إلى Compute Engine > "آلات VM الافتراضية" في Google Cloud Console. من المفترض أن يظهر ollama-instance مع علامة اختيار خضراء تشير إلى أنّه قيد التشغيل. إذا لم تتمكّن من رؤيته، تأكَّد من أنّ المنطقة هي us-central1. إذا لم يظهر لك، قد تحتاج إلى البحث عنه.

قائمة Compute Engine

👉سنثبّت أصغر نموذج DeepSeek ونختبره، ثم نعود إلى "محرّر Cloud Shell"، وفي نافذة طرفية جديدة، ننفّذ الأمر التالي لتسجيل الدخول إلى مثيل GCE باستخدام SSH.

gcloud compute ssh ollama-instance --zone=us-central1-a

بعد إنشاء اتصال SSH، قد يُطلب منك إجراء ما يلي:

"هل تريد المتابعة (نعم/لا)؟"

ما عليك سوى كتابة Y(غير حساسة لحالة الأحرف) والضغط على Enter للمتابعة.

بعد ذلك، قد يُطلب منك إنشاء عبارة مرور لمفتاح SSH. إذا كنت تفضّل عدم استخدام عبارة مرور، ما عليك سوى الضغط على Enter مرّتين لقبول الإعداد التلقائي (بدون عبارة مرور).

👉الآن بعد أن أصبحت في الجهاز الافتراضي، اسحب أصغر نموذج DeepSeek R1 واختبِر ما إذا كان يعمل.

ollama pull deepseek-r1:1.5b
ollama run deepseek-r1:1.5b "who are you?"

👉اخرج من مثيل GCE وأدخِل ما يلي في وحدة طرفية SSH:

exit

👉بعد ذلك، عليك إعداد سياسة الشبكة، حتى تتمكّن الخدمات الأخرى من الوصول إلى النموذج اللغوي الكبير. يُرجى حصر الوصول إلى الآلة الافتراضية إذا كنت تريد إجراء ذلك في مرحلة الإنتاج، أو تنفيذ تسجيل دخول آمن للخدمة أو حصر الوصول إلى عناوين IP. التشغيل:

gcloud compute firewall-rules create allow-ollama-11434 \
    --allow=tcp:11434 \
    --target-tags=ollama \
    --description="Allow access to Ollama on port 11434"

👉للتأكّد من عمل سياسة جدار الحماية بشكلٍ سليم، جرِّب تنفيذ ما يلي:

export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
curl -X POST "${OLLAMA_HOST}/api/generate" \
     -H "Content-Type: application/json" \
     -d '{
          "prompt": "Hello, what are you?",
          "model": "deepseek-r1:1.5b",
          "stream": false
        }'

بعد ذلك، سنعمل على وظيفة Deepseek في وكيل المهام لإنشاء مهام تركّز على العمل الفردي.

👉عدِّل deepseek.py ضمن مجلد assignment وأضِف المقتطف التالي إلى النهاية:

def gen_assignment_deepseek(state):
    print(f"---------------gen_assignment_deepseek")

    template = """
        You are an instructor who favor student to focus on individual work.

        Develop engaging and practical assignments for each week, ensuring they align with the teaching plan's objectives and progressively build upon each other.  

        For each week, provide the following:

        * **Week [Number]:** A descriptive title for the assignment (e.g., "Data Exploration Project," "Model Building Exercise").
        * **Learning Objectives Assessed:** List the specific learning objectives from the teaching plan that this assignment assesses.
        * **Description:** A detailed description of the task, including any specific requirements or constraints.  Provide examples or scenarios if applicable.
        * **Deliverables:** Specify what students need to submit (e.g., code, report, presentation).
        * **Estimated Time Commitment:**  The approximate time students should dedicate to completing the assignment.
        * **Assessment Criteria:** Briefly outline how the assignment will be graded (e.g., correctness, completeness, clarity, creativity).

        The assignments should be a mix of individual and collaborative work where appropriate.  Consider different learning styles and provide opportunities for students to apply their knowledge creatively.

        Based on this teaching plan: {teaching_plan}
        """

    
    prompt = ChatPromptTemplate.from_template(template)

    model = OllamaLLM(model="deepseek-r1:1.5b",
                   base_url=OLLAMA_HOST)

    chain = prompt | model


    response = chain.invoke({"teaching_plan":state["teaching_plan"]})
    state["model_two_assignment"] = response
    
    return state

import unittest

class TestGenAssignmentDeepseek(unittest.TestCase):
    def test_gen_assignment_deepseek(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}

        updated_state = gen_assignment_deepseek(initial_state)

        self.assertIn("model_two_assignment", updated_state)
        self.assertIsNotNone(updated_state["model_two_assignment"])
        self.assertIsInstance(updated_state["model_two_assignment"], str)
        self.assertGreater(len(updated_state["model_two_assignment"]), 0)
        print(updated_state["model_two_assignment"])


if __name__ == '__main__':
    unittest.main()

👉لنختبر ذلك من خلال تنفيذ الأمر التالي:

cd ~/aidemy-bootstrap/assignment
source env/bin/activate
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
python deepseek.py

من المفترض أن تظهر لك مهمة تتضمّن المزيد من العمل الذاتي.

**Assignment Plan for Each Week**

---

### **Week 1: 2D Shapes and Angles**
- **Week Title:** "Exploring 2D Shapes"
Assign students to research and present on various 2D shapes. Include a project where they create models using straws and tape for triangles, draw quadrilaterals with specific measurements, and compare their properties. 

### **Week 2: 3D Shapes and Symmetry**
Assign students to create models or nets for cubes and cuboids. They will also predict how folding these nets form the 3D shapes. Include a project where they identify symmetrical properties using mirrors or folding techniques.

### **Week 3: Position, Direction, and Problem Solving**

Assign students to use mirrors or folding techniques for reflections. Include activities where they measure angles, use a protractor, solve problems involving perimeter/area, and create symmetrical designs.
....

👉أوقِف ctl+c، ونظِّف رمز الاختبار. أزِل الرمز التالي من deepseek.py

import unittest

class TestGenAssignmentDeepseek(unittest.TestCase):
    def test_gen_assignment_deepseek(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}

        updated_state = gen_assignment_deepseek(initial_state)

        self.assertIn("model_two_assignment", updated_state)
        self.assertIsNotNone(updated_state["model_two_assignment"])
        self.assertIsInstance(updated_state["model_two_assignment"], str)
        self.assertGreater(len(updated_state["model_two_assignment"]), 0)
        print(updated_state["model_two_assignment"])


if __name__ == '__main__':
    unittest.main()

الآن، سنستخدم نموذج Gemini نفسه لدمج المهمتين في مهمة جديدة. عدِّل الملف gemini.py الموجود في المجلد assignment.

👉ألصِق الرمز التالي في نهاية ملف gemini.py:

def combine_assignments(state):
    print(f"---------------combine_assignments ")
    region=get_next_region()
    client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
    response = client.models.generate_content(
        model=MODEL_ID, contents=f"""
        Look at all the proposed assignment so far {state["model_one_assignment"]} and {state["model_two_assignment"]}, combine them and come up with a final assignment for student. 
        """
    )

    state["final_assignment"] = response.text
    
    return state

لدمج مزايا النموذجين، سننظّم سير عمل محدّدًا باستخدام LangGraph. تتألف سير العمل هذه من ثلاث خطوات: أولاً، ينشئ نموذج Gemini مهمة تركّز على التعاون، وثانيًا، ينشئ نموذج DeepSeek مهمة تركّز على العمل الفردي، وأخيرًا، يدمج Gemini هاتين المهمتين في مهمة واحدة شاملة. بما أنّنا نحدّد مسبقًا تسلسل الخطوات بدون اتّخاذ قرارات من خلال النماذج اللغوية الكبيرة، يشكّل ذلك تنسيقًا أحادي المسار يحدّده المستخدم.

نظرة عامة على دمج Langraph

👉ألصِق الرمز التالي في نهاية main.pyالملف ضمن مجلد assignment:

def create_assignment(teaching_plan: str):
    print(f"create_assignment---->{teaching_plan}")
    builder = StateGraph(State)
    builder.add_node("gen_assignment_gemini", gen_assignment_gemini)
    builder.add_node("gen_assignment_deepseek", gen_assignment_deepseek)
    builder.add_node("combine_assignments", combine_assignments)
    
    builder.add_edge(START, "gen_assignment_gemini")
    builder.add_edge("gen_assignment_gemini", "gen_assignment_deepseek")
    builder.add_edge("gen_assignment_deepseek", "combine_assignments")
    builder.add_edge("combine_assignments", END)

    graph = builder.compile()
    state = graph.invoke({"teaching_plan": teaching_plan})

    return state["final_assignment"]



import unittest

class TestCreateAssignment(unittest.TestCase):
    def test_create_assignment(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
        updated_state = create_assignment(initial_state)
        
        print(updated_state)


if __name__ == '__main__':
    unittest.main()

👉لاختبار وظيفة create_assignment في البداية والتأكّد من أنّ سير العمل الذي يجمع بين Gemini وDeepSeek يعمل بشكل صحيح، نفِّذ الأمر التالي:

cd ~/aidemy-bootstrap/assignment
source env/bin/activate
pip install -r requirements.txt
python main.py

من المفترض أن يظهر لك شيء يجمع بين النموذجين مع مراعاة وجهة نظر كل منهما بشأن دراسة الطلاب وأعمال المجموعات الطلابية.

**Tasks:**

1. **Clue Collection:** Gather all the clues left by the thieves. These clues will include:
    * Descriptions of shapes and their properties (angles, sides, etc.)
    * Coordinate grids with hidden messages
    * Geometric puzzles requiring transformation (translation, reflection, rotation)
    * Challenges involving area, perimeter, and angle calculations

2. **Clue Analysis:** Decipher each clue using your geometric knowledge. This will involve:
    * Identifying the shape and its properties
    * Plotting coordinates and interpreting patterns on the grid
    * Solving geometric puzzles by applying transformations
    * Calculating area, perimeter, and missing angles 

3. **Case Report:** Create a comprehensive case report outlining your findings. This report should include:
    * A detailed explanation of each clue and its solution
    * Sketches and diagrams to support your explanations
    * A step-by-step account of how you followed the clues to locate the artifact
    * A final conclusion about the thieves and their motives

👉أوقِف ctl+c، ونظِّف رمز الاختبار. أزِل الرمز التالي من main.py

import unittest

class TestCreateAssignment(unittest.TestCase):
    def test_create_assignment(self):
        test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
        initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
        updated_state = create_assignment(initial_state)
        
        print(updated_state)


if __name__ == '__main__':
    unittest.main()

Generate Assignment.png

لجعل عملية إنشاء المهام تلقائية ومتجاوبة مع خطط التدريس الجديدة، سنستفيد من بنية الأحداث الحالية. يعرّف الرمز التالي دالة Cloud Run (generate_assignment) سيتم تشغيلها كلما تم نشر خطة تدريس جديدة في موضوع Pub/Sub‏ "plan".

👉أضِف الرمز التالي إلى نهاية main.py في المجلد assignment:

@functions_framework.cloud_event
def generate_assignment(cloud_event):
    print(f"CloudEvent received: {cloud_event.data}")

    try:
        if isinstance(cloud_event.data.get('message', {}).get('data'), str): 
            data = json.loads(base64.b64decode(cloud_event.data['message']['data']).decode('utf-8'))
            teaching_plan = data.get('teaching_plan')
        elif 'teaching_plan' in cloud_event.data: 
            teaching_plan = cloud_event.data["teaching_plan"]
        else:
            raise KeyError("teaching_plan not found") 

        assignment = create_assignment(teaching_plan)

        print(f"Assignment---->{assignment}")

        #Store the return assignment into bucket as a text file
        storage_client = storage.Client()
        bucket = storage_client.bucket(ASSIGNMENT_BUCKET)
        file_name = f"assignment-{random.randint(1, 1000)}.txt"
        blob = bucket.blob(file_name)
        blob.upload_from_string(assignment)

        return f"Assignment generated and stored in {ASSIGNMENT_BUCKET}/{file_name}", 200

    except (json.JSONDecodeError, AttributeError, KeyError) as e:
        print(f"Error decoding CloudEvent data: {e} - Data: {cloud_event.data}")
        return "Error processing event", 500

    except Exception as e:
        print(f"Error generate assignment: {e}")
        return "Error generate assignment", 500

الاختبار على الجهاز

قبل النشر على Google Cloud، من الممارسات الجيدة اختبار Cloud Run Function محليًا. ويتيح ذلك تكرارًا أسرع وتصحيحًا أسهل للأخطاء.

أولاً، أنشئ حزمة Cloud Storage لتخزين ملفات المهام التي تم إنشاؤها، ثم امنح حساب الخدمة إذن الوصول إلى الحزمة. نفِّذ الأوامر التالية في الوحدة الطرفية:

👉ملاحظة مهمة: احرص على تحديد اسم فريد ASSIGNMENT_BUCKET يبدأ بـ "aidemy-assignment-". هذا الاسم الفريد مهم لتجنُّب تعارض الأسماء عند إنشاء حزمة Cloud Storage. (استبدِل <YOUR_NAME> بأي كلمة عشوائية)

export ASSIGNMENT_BUCKET=aidemy-assignment-<YOUR_NAME> #Name must be unqiue

👉ثم نفِّذ الأمر التالي:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gsutil mb -p $PROJECT_ID -l us-central1 gs://$ASSIGNMENT_BUCKET

gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
    --member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
    --role "roles/storage.objectViewer"

gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
    --member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
    --role "roles/storage.objectCreator"

👉الآن، ابدأ محاكي "دالة Cloud Run":

cd ~/aidemy-bootstrap/assignment
functions-framework \
    --target generate_assignment \
    --signature-type=cloudevent \
    --source main.py

👉أثناء تشغيل المحاكي في إحدى وحدات الترميز، افتح وحدة ترميز ثانية في Cloud Shell. في نافذة المحطة الطرفية الثانية، أرسِل حدث CloudEvent تجريبيًا إلى المحاكي لمحاكاة نشر خطة تدريس جديدة:

طرفان

  curl -X POST \
  http://localhost:8080/ \
  -H "Content-Type: application/json" \
  -H "ce-id: event-id-01" \
  -H "ce-source: planner-agent" \
  -H "ce-specversion: 1.0" \
  -H "ce-type: google.cloud.pubsub.topic.v1.messagePublished" \
  -d '{
    "message": {
      "data": "eyJ0ZWFjaGluZ19wbGFuIjogIldlZWsgMTogMkQgU2hhcGVzIGFuZCBBbmdsZXMgLSBEYXkgMTogUmV2aWV3IG9mIGJhc2ljIDJEIHNoYXBlcyAoc3F1YXJlcywgcmVjdGFuZ2xlcywgdHJpYW5nbGVzLCBjaXJjbGVzKS4gRGF5IDI6IEV4cGxvcmluZyBkaWZmZXJlbnQgdHlwZXMgb2YgdHJpYW5nbGVzIChlcXVpbGF0ZXJhbCwgaXNvc2NlbGVzLCBzY2FsZW5lLCByaWdodC1hbmdsZWQpLiBEYXkgMzogRXhwbG9yaW5nIHF1YWRyaWxhdGVyYWxzIChzcXVhcmUsIHJlY3RhbmdsZSwgcGFyYWxsZWxvZ3JhbSwgcmhvbWJ1cywgdHJhcGV6aXVtKS4gRGF5IDQ6IEludHJvZHVjdGlvbiB0byBhbmdsZXM6IHJpZ2h0IGFuZ2xlcywgYWN1dGUgYW5nbGVzLCBhbmQgb2J0dXNlIGFuZ2xlcy4gRGF5IDU6IE1lYXN1cmluZyBhbmdsZXMgdXNpbmcgYSBwcm90cmFjdG9yLiBXZWVrIDI6IDNEIFNoYXBlcyBhbmQgU3ltbWV0cnkgLSBEYXkgNjogSW50cm9kdWN0aW9uIHRvIDNEIHNoYXBlczogY3ViZXMsIGN1Ym9pZHMsIHNwaGVyZXMsIGN5bGluZGVycywgY29uZXMsIGFuZCBweXJhbWlkcy4gRGF5IDc6IERlc2NyaWJpbmcgM0Qgc2hhcGVzIHVzaW5nIGZhY2VzLCBlZGdlcywgYW5kIHZlcnRpY2VzLiBEYXkgODogUmVsYXRpbmcgMkQgc2hhcGVzIHRvIDNEIHNoYXBlcy4gRGF5IDk6IElkZW50aWZ5aW5nIGxpbmVzIG9mIHN5bW1ldHJ5IGluIDJEIHNoYXBlcy4gRGF5IDEwOiBDb21wbGV0aW5nIHN5bW1ldHJpY2FsIGZpZ3VyZXMuIFdlZWsgMzogUG9zaXRpb24sIERpcmVjdGlvbiwgYW5kIFByb2JsZW0gU29sdmluZyAtIERheSAxMTogRGVzY3JpYmluZyBwb3NpdGlvbiB1c2luZyBjb29yZGluYXRlcyBpbiB0aGUgZmlyc3QgcXVhZHJhbnQuIERheSAxMjogUGxvdHRpbmcgY29vcmRpbmF0ZXMgdG8gZHJhdyBzaGFwZXMuIERheSAxMzogVW5kZXJzdGFuZGluZyB0cmFuc2xhdGlvbiAoc2xpZGluZyBhIHNoYXBlKS4gRGF5IDE0OiBVbmRlcnN0YW5kaW5nIHJlZmxlY3Rpb24gKGZsaXBwaW5nIGEgc2hhcGUpLiBEYXkgMTU6IFByb2JsZW0tc29sdmluZyBhY3Rpdml0aWVzIGludm9sdmluZyBwZXJpbWV0ZXIsIGFyZWEsIGFuZCBtaXNzaW5nIGFuZ2xlcy4ifQ=="
    }
  }'

بدلاً من التحديق في الشاشة أثناء انتظار الردّ، انتقِل إلى نافذة Cloud Shell الأخرى. يمكنك مراقبة مستوى التقدّم وأي رسائل ناتجة أو رسائل خطأ تم إنشاؤها بواسطة الدالة في نافذة المحاكي. 😁

يجب أن يعرض الأمر curl الردّ "OK" (بدون سطر جديد، لذا قد يظهر الردّ "OK" في السطر نفسه الذي يظهر فيه طلب موجه من shell في الجهاز).

للتأكّد من أنّه تم إنشاء المهمة وتخزينها بنجاح، انتقِل إلى Google Cloud Console ثم إلى التخزين > "Cloud Storage". اختَر حزمة aidemy-assignment التي أنشأتها. من المفترض أن يظهر لك ملف نصي باسم assignment-{random number}.txt في الحزمة. انقر على الملف لتنزيله والتحقّق من محتواه. يتحقّق هذا الإجراء من أنّ الملف الجديد يتضمّن مهمة دراسية جديدة تم إنشاؤها للتو.

12-01-assignment-bucket

👉في الوحدة الطرفية التي يتم تشغيل المحاكي فيها، اكتب ctrl+c للخروج. وأغلِق الوحدة الطرفية الثانية. 👉في الوحدة الطرفية التي يتم تشغيل المحاكي فيها، اخرج من البيئة الافتراضية.

deactivate

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

👈بعد ذلك، سننشر وكيل التعيين على السحابة الإلكترونية

cd ~/aidemy-bootstrap/assignment
export ASSIGNMENT_BUCKET=$(gcloud storage buckets list --format="value(name)" | grep aidemy-assignment)
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud functions deploy assignment-agent \
 --gen2 \
 --timeout=540 \
 --memory=2Gi \
 --cpu=1 \
 --set-env-vars="ASSIGNMENT_BUCKET=${ASSIGNMENT_BUCKET}" \
 --set-env-vars=GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} \
 --set-env-vars=OLLAMA_HOST=${OLLAMA_HOST} \
 --region=us-central1 \
 --runtime=python312 \
 --source=. \
 --entry-point=generate_assignment \
 --trigger-topic=plan 

تحقَّق من عملية النشر من خلال الانتقال إلى Google Cloud Console، ثم إلى Cloud Run. من المفترض أن تظهر خدمة جديدة باسم courses-agent. 12-03-function-list

بعد تنفيذ سير عمل إنشاء المهام واختباره ونشره، يمكننا الانتقال إلى الخطوة التالية، وهي إتاحة هذه المهام في بوابة الطالب.

14. اختياري: التعاون المستند إلى الأدوار باستخدام Gemini وDeepSeek - تتمة

إنشاء مواقع إلكترونية ديناميكية

لتحسين بوابة الطلاب وجعلها أكثر تفاعلية، سننفّذ عملية إنشاء ديناميكية لملفات HTML الخاصة بصفحات المهام. والهدف هو تحديث البوابة تلقائيًا بتصميم جديد وجذّاب بصريًا كلما تم إنشاء مهمة جديدة. يستفيد هذا الإجراء من إمكانات الترميز الخاصة بالنماذج اللغوية الكبيرة لإنشاء تجربة مستخدم أكثر ديناميكية وإثارة للاهتمام.

14-01-generate-html

👉في Cloud Shell Editor، عدِّل الملف render.py ضمن المجلد portal، واستبدِل

def render_assignment_page():
    return ""

مع مقتطف الرمز التالي:

def render_assignment_page(assignment: str):
    try:
        region=get_next_region()
        llm = VertexAI(model_name="gemini-2.0-flash-001", location=region)
        input_msg = HumanMessage(content=[f"Here the assignment {assignment}"])
        prompt_template = ChatPromptTemplate.from_messages(
            [
                SystemMessage(
                    content=(
                        """
                        As a frontend developer, create HTML to display a student assignment with a creative look and feel. Include the following navigation bar at the top:
                        ```
                        <nav>
                            <a href="/">Home</a>
                            <a href="/quiz">Quizzes</a>
                            <a href="/courses">Courses</a>
                            <a href="/assignment">Assignments</a>
                        </nav>
                        ```
                        Also include these links in the <head> section:
                        ```
                        <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
                        <link rel="preconnect" href="https://fonts.googleapis.com">
                        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
                        <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">

                        ```
                        Do not apply inline styles to the navigation bar. 
                        The HTML should display the full assignment content. In its CSS, be creative with the rainbow colors and aesthetic. 
                        Make it creative and pretty
                        The assignment content should be well-structured and easy to read.
                        respond with JUST the html file
                        """
                    )
                ),
                input_msg,
            ]
        )

        prompt = prompt_template.format()
        
        response = llm.invoke(prompt)

        response = response.replace("```html", "")
        response = response.replace("```", "")
        with open("templates/assignment.html", "w") as f:
            f.write(response)


        print(f"response: {response}")

        return response
    except Exception as e:
        print(f"Error sending message to chatbot: {e}") # Log this error too!
        return f"Unable to process your request at this time. Due to the following reason: {str(e)}"

يستخدم نموذج Gemini لإنشاء رمز HTML للمهمة بشكل ديناميكي. يستخدم محتوى المهمة كمدخل ويستعين بطلب لإصدار تعليمات إلى Gemini من أجل إنشاء صفحة HTML جذابة بصريًا بأسلوب إبداعي.

بعد ذلك، سننشئ نقطة نهاية يتم تشغيلها كلما تمت إضافة مستند جديد إلى مجموعة المهام:

👉في مجلد البوابة، عدِّل الملف app.py واستبدِل السطر ## REPLACE ME! RENDER ASSIGNMENT بالرمز التالي:

@app.route('/render_assignment', methods=['POST'])
def render_assignment():
    try:
        data = request.get_json()
        file_name = data.get('name')
        bucket_name = data.get('bucket')

        if not file_name or not bucket_name:
            return jsonify({'error': 'Missing file name or bucket name'}), 400

        storage_client = storage.Client()
        bucket = storage_client.bucket(bucket_name)
        blob = bucket.blob(file_name)
        content = blob.download_as_text()

        print(f"File content: {content}")

        render_assignment_page(content)

        return jsonify({'message': 'Assignment rendered successfully'})

    except Exception as e:
        print(f"Error processing file: {e}")
        return jsonify({'error': 'Error processing file'}), 500

عند تشغيلها، تسترد اسم الملف واسم الحزمة من بيانات الطلب، وتنزّل محتوى المهمة من Cloud Storage، وتستدعي الدالة render_assignment_page لإنشاء رمز HTML.

👉سننفّذها محليًا:

cd ~/aidemy-bootstrap/portal
source env/bin/activate
python app.py

👉من قائمة "معاينة الويب" في أعلى نافذة Cloud Shell، اختَر "المعاينة على المنفذ 8080". سيؤدي ذلك إلى فتح تطبيقك في علامة تبويب جديدة في المتصفّح. انتقِل إلى رابط المهمة في شريط التنقّل. من المفترض أن تظهر لك صفحة فارغة في هذه المرحلة، وهذا السلوك متوقّع لأنّنا لم ننشئ بعد جسر التواصل بين وكيل التعيين والبوابة لتعبئة المحتوى ديناميكيًا.

14-02-deployment-overview

o يمكنك إيقاف النص البرمجي بالضغط على Ctrl+C.

👉لدمج هذه التغييرات ونشر الرمز المعدَّل، أعِد إنشاء صورة وكيل البوابة وادفعها:

cd ~/aidemy-bootstrap/portal/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal

👉بعد إرسال الصورة الجديدة، أعِد نشر خدمة Cloud Run. شغِّل النص البرمجي التالي لفرض تحديث Cloud Run:

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud run services update aidemy-portal \
    --region=us-central1 \
    --set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME

👉سننشئ الآن مشغّلاً في Eventarc يستمع إلى أي عنصر جديد يتم إنشاؤه (إكماله) في حزمة المهام. سيؤدي هذا المشغّل تلقائيًا إلى استدعاء نقطة النهاية /render_assignment في خدمة البوابة عند إنشاء ملف مهمة جديد.

gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$(gcloud storage service-agent --project $PROJECT_ID)" \
  --role="roles/pubsub.publisher"
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud eventarc triggers create portal-assignment-trigger \
--location=us-central1 \
--service-account=$SERVICE_ACCOUNT_NAME \
--destination-run-service=aidemy-portal \
--destination-run-region=us-central1 \
--destination-run-path="/render_assignment" \
--event-filters="bucket=$ASSIGNMENT_BUCKET" \
--event-filters="type=google.cloud.storage.object.v1.finalized"

للتأكّد من إنشاء المشغّل بنجاح، انتقِل إلى صفحة مشغّلات Eventarc في Google Cloud Console. من المفترض أن يظهر لك portal-assignment-trigger في الجدول. انقر على اسم المشغّل للاطّلاع على تفاصيله. مشغّل التعيين

قد يستغرق تفعيل المشغّل الجديد مدة تتراوح بين دقيقتَين و3 دقائق.

لمشاهدة عملية إنشاء التعيين الديناميكي أثناء التنفيذ، شغِّل الأمر التالي للعثور على عنوان URL الخاص بعميل المخطّط (إذا لم يكن متوفّرًا لديك):

gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner

ابحث عن عنوان URL الخاص بوكيل البوابة باتّباع الخطوات التالية:

gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal

في وكيل التخطيط، أنشئ خطة تدريس جديدة.

13-02-assignment

بعد بضع دقائق (لإتاحة إكمال إنشاء الصوت والواجب وعرض HTML)، انتقِل إلى بوابة الطالب.

👉انقر على رابط "المهمة" في شريط التنقّل. من المفترض أن تظهر لك مهمة تم إنشاؤها حديثًا باستخدام لغة HTML تم إنشاؤها ديناميكيًا. في كل مرة يتم فيها إنشاء خطة تدريس، يجب أن تكون مهمة ديناميكية.

13-02-assignment

تهانينا على إكمال نظام Aidemy المتعدد الوكلاء. لقد اكتسبت خبرة عملية ومعلومات قيّمة حول ما يلي:

  • فوائد الأنظمة المتعددة الوكلاء، بما في ذلك التجزئة وقابلية التوسّع والتخصّص والصيانة المبسّطة
  • أهمية البنى المستندة إلى الأحداث في إنشاء تطبيقات متجاوبة ومرتبطة بشكل غير محكم
  • الاستخدام الاستراتيجي للنماذج اللغوية الكبيرة، أي اختيار النموذج المناسب للمهمة ودمجه مع أدوات لتحقيق تأثير في العالم الحقيقي
  • ممارسات التطوير المتوافق مع السحابة الإلكترونية باستخدام خدمات Google Cloud لإنشاء حلول قابلة للتطوير وموثوقة
  • أهمية مراعاة خصوصية البيانات ونماذج الاستضافة الذاتية كبديل لحلول المورّدين

أصبح لديك الآن أساس قوي لإنشاء تطبيقات متطورة مستندة إلى الذكاء الاصطناعي على Google Cloud.

15. التحديات والخطوات التالية

تهانينا على إنشاء نظام Aidemy المستند إلى عدّة وكلاء. لقد وضعت أساسًا متينًا للتعليم المستنِد إلى الذكاء الاصطناعي. لنستعرض الآن بعض التحديات والتحسينات المحتملة في المستقبل لتوسيع إمكاناته بشكل أكبر وتلبية الاحتياجات الواقعية:

التعلم التفاعلي من خلال جلسات الأسئلة والأجوبة المباشرة:

  • التحدي: هل يمكنك الاستفادة من واجهة برمجة التطبيقات Live في Gemini 2 لإنشاء ميزة "أسئلة وأجوبة" في الوقت الفعلي للطلاب؟ تخيَّل صفًا افتراضيًا يمكن فيه للطلاب طرح الأسئلة وتلقّي إجابات فورية مستندة إلى الذكاء الاصطناعي.

إرسال المهام ووضع الدرجات تلقائيًا:

  • التحدي: تصميم نظام وتنفيذه يتيح للطلاب إرسال الواجبات رقميًا ووضع الدرجات لها تلقائيًا باستخدام الذكاء الاصطناعي، مع توفير آلية لرصد السرقة الأدبية ومنعها توفّر هذه المشكلة فرصة رائعة لاستكشاف التوليد المعزّز بالاسترجاع (RAG) بهدف تحسين دقة وموثوقية عمليات التقييم ورصد السرقة الأدبية.

aidemy-climb

16. تَنظيم

بعد أن أنشأنا نظام Aidemy المتعدّد الوكلاء واستكشفناه، حان الوقت لتنظيف بيئة Google Cloud.

👉حذف خدمات Cloud Run

gcloud run services delete aidemy-planner --region=us-central1 --quiet
gcloud run services delete aidemy-portal --region=us-central1 --quiet
gcloud run services delete courses-agent --region=us-central1 --quiet
gcloud run services delete book-provider --region=us-central1 --quiet
gcloud run services delete assignment-agent --region=us-central1 --quiet

👉حذف مشغّل Eventarc

gcloud eventarc triggers delete portal-assignment-trigger --location=us --quiet
gcloud eventarc triggers delete plan-topic-trigger --location=us-central1 --quiet
gcloud eventarc triggers delete portal-assignment-trigger --location=us-central1 --quiet
ASSIGNMENT_AGENT_TRIGGER=$(gcloud eventarc triggers list --project="$PROJECT_ID" --location=us-central1 --filter="name:assignment-agent" --format="value(name)")
COURSES_AGENT_TRIGGER=$(gcloud eventarc triggers list --project="$PROJECT_ID" --location=us-central1 --filter="name:courses-agent" --format="value(name)")
gcloud eventarc triggers delete $ASSIGNMENT_AGENT_TRIGGER --location=us-central1 --quiet
gcloud eventarc triggers delete $COURSES_AGENT_TRIGGER --location=us-central1 --quiet

👉حذف موضوع Pub/Sub

gcloud pubsub topics delete plan --project="$PROJECT_ID" --quiet

👈حذف مثيل Cloud SQL

gcloud sql instances delete aidemy --quiet

👉حذف مستودع Artifact Registry

gcloud artifacts repositories delete agent-repository --location=us-central1 --quiet

👉حذف الأسرار في Secret Manager

gcloud secrets delete db-user --quiet
gcloud secrets delete db-pass --quiet
gcloud secrets delete db-name --quiet

👈حذف مثيل Compute Engine (إذا تم إنشاؤه لـ Deepseek)

gcloud compute instances delete ollama-instance --zone=us-central1-a --quiet

👉حذف قاعدة جدار الحماية الخاصة بمثيل Deepseek

gcloud compute firewall-rules delete allow-ollama-11434 --quiet

👉حذف حِزم Cloud Storage

export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export ASSIGNMENT_BUCKET=$(gcloud storage buckets list --format="value(name)" | grep aidemy-assignment)
gsutil rm -r gs://$COURSE_BUCKET_NAME
gsutil rm -r gs://$ASSIGNMENT_BUCKET

aidemy-broom