Aidemy: פיתוח מערכות עם מספר סוכנים באמצעות LangGraph,‏ EDA ו-AI גנרטיבי ב-Google Cloud

1. מבוא

שלום! אז אתם מתעניינים באפשרות של סוכנים – עוזרים קטנים שיכולים לבצע בשבילכם משימות בלי שתצטרכו להרים אצבע, נכון? מדהים! אבל בואו נהיה ריאליים, סוכן אחד לא תמיד יספיק, במיוחד כשעובדים על פרויקטים גדולים ומורכבים יותר. כנראה שתצטרכו צוות שלם של אנשים כאלה! כאן נכנסות לתמונה מערכות מרובות סוכנים.

סוכנים שמבוססים על LLM מעניקים גמישות מדהימה בהשוואה לקידוד קשיח מהסוג הישן. אבל, ותמיד יש אבל, יש להם אתגרים משלהם. בסדנה הזו נסביר בדיוק איך עושים את זה.

title

אלה הנושאים שתוכלו ללמוד – אפשר לראות את זה כהעלאת הרמה שלכם בשימוש בסוכן:

יצירת הסוכן הראשון באמצעות LangGraph: נלמד איך ליצור סוכן משלכם באמצעות LangGraph, מסגרת פופולרית. תלמדו איך ליצור כלים שמתחברים למסדי נתונים, איך להשתמש ב-Gemini 2 API כדי לבצע חיפושים באינטרנט, ואיך לשפר את ההנחיות והתשובות כדי שהסוכן יוכל ליצור אינטראקציה לא רק עם מודלים של שפה גדולה (LLM) אלא גם עם שירותים קיימים. נסביר גם איך פועלת הפונקציה function calling.

תזמור סוכנים בדרך שלכם: נסקור דרכים שונות לתזמור הסוכנים, החל מנתיבים פשוטים ועד לתרחישים מורכבים יותר עם כמה נתיבים. אפשר לחשוב על זה כמו ניהול של צוות הסוכנים.

מערכות מרובות סוכנים: תלמדו איך להגדיר מערכת שבה הסוכנים יכולים לשתף פעולה ולבצע משימות ביחד – והכול בזכות ארכיטקטורה מבוססת-אירועים.

גמישות בשימוש ב-LLM: אנחנו משתמשים ב-LLM הכי מתאים למשימה, ולא נשארים עם LLM אחד בלבד. תלמדו איך להשתמש בכמה מודלים של שפה גדולה (LLM), להקצות להם תפקידים שונים ולשפר את יכולת פתרון הבעיות באמצעות "מודלים של חשיבה" מעניינים.

מהו תוכן דינמי? אין בעיה!: תארו לעצמכם סוכן שיוצר תוכן דינמי שמותאם במיוחד לכל משתמש, בזמן אמת. נסביר לכם איך עושים את זה.

מעבר לענן עם Google Cloud: לא מדובר רק בהתנסות במחברת. נראה לכם איך לתכנן ולפרוס את מערכת המולטי-אגנטים שלכם ב-Google Cloud כדי שהיא תהיה מוכנה לשימוש בעולם האמיתי.

הפרויקט הזה יהיה דוגמה טובה לשימוש בכל הטכניקות שדיברנו עליהן.

2. ארכיטקטורה

להיות מורה או לעבוד בתחום החינוך יכול להיות מאוד מתגמל, אבל בואו נודה בזה, עומס העבודה, במיוחד כל עבודת ההכנה, יכול להיות מאתגר! בנוסף, לרוב אין מספיק צוות, והשיעורים הפרטיים יכולים להיות יקרים. לכן אנחנו מציעים עוזר הוראה מבוסס-AI. הכלי הזה יכול להקל על עומס העבודה של אנשי החינוך ולעזור לגשר על הפער שנוצר בגלל מחסור בצוותים ובהדרכה במחירים סבירים.

העוזר הדיגיטלי שלנו מבוסס-AI ויכול ליצור תוכניות לימודים מפורטות, חידונים מהנים, סיכומי אודיו קלים להבנה ומשימות מותאמות אישית. כך המורים יכולים להתמקד במה שהם עושים הכי טוב: ליצור קשר עם התלמידים ולעזור להם להתאהב בלמידה.

במערכת יש שני אתרים: אתר אחד למורים ליצירת תוכניות לימודים לשבועות הקרובים,

מתכנן

ואחד לתלמידים, שדרכו הם יכולים לגשת לבחנים, לסיכומי אודיו ולמטלות. שער

בסדר, בואו נסביר את הארכיטקטורה שמפעילה את העוזר האישי שלנו להוראה, Aidemy. כפי שניתן לראות, פירקנו את התהליך לכמה רכיבים מרכזיים, שפועלים יחד כדי להשיג את התוצאה הזו.

ארכיטקטורה

Key Architectural Elements and Technologies:

Google Cloud Platform ‏ (GCP): מרכזי לכל המערכת:

  • ‫Vertex AI: גישה למודלים גדולים של שפה (LLM) של Gemini מבית Google.
  • ‫Cloud Run: פלטפורמה ללא שרת (serverless) לפריסה של סוכנים ופונקציות בקונטיינרים.
  • ‫Cloud SQL: מסד נתונים של PostgreSQL לנתוני תוכנית הלימודים.
  • ‫Pub/Sub ו-Eventarc: הבסיס לארכיטקטורה מבוססת-אירועים, שמאפשרת תקשורת אסינכרונית בין רכיבים.
  • אחסון בענן: אחסון של סיכומי אודיו וקבצים של מטלות.
  • ‫Secret Manager: ניהול מאובטח של פרטי כניסה למסד נתונים.
  • ‫Artifact Registry: אחסון קובצי אימג' של Docker עבור הסוכנים.
  • ‫Compute Engine: כדי לפרוס LLM באירוח עצמי במקום להסתמך על פתרונות של ספקים

מודלים גדולים של שפה (LLM): ה "מוח" של המערכת:

  • מודלים של Gemini מבית Google:‏ (Gemini x Pro, ‏ Gemini x Flash, ‏ Gemini x Flash Thinking) משמשים לתכנון שיעורים, ליצירת תוכן, ליצירת HTML דינמי, להסבר על חידונים ולשילוב המטלות.
  • ‫DeepSeek: נעשה בו שימוש למשימה הספציפית של יצירת מטלות ללימוד עצמי

LangChain ו-LangGraph: מסגרות לפיתוח אפליקציות LLM

  • מאפשרת ליצור תהליכי עבודה מורכבים עם כמה סוכנים.
  • הפעלה של תזמור חכם של כלים (קריאות ל-API, שאילתות במסד נתונים, חיפושים באינטרנט).
  • הטמעה של ארכיטקטורה מבוססת-אירועים לצורך מדרגיות וגמישות של המערכת.

הארכיטקטורה שלנו משלבת את היכולות של מודלים גדולים של שפה עם נתונים מובְנים ותקשורת מבוססת-אירועים, והכול פועל ב-Google Cloud. כך אנחנו יכולים לבנות עוזר הוראה יעיל, אמין וניתן להרחבה.

3. לפני שמתחילים

במסוף Google Cloud, בדף לבחירת הפרויקט, בוחרים או יוצרים פרויקט ב-Google Cloud. הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud. איך בודקים אם החיוב מופעל בפרויקט

הפעלת Gemini Code Assist ב-Cloud Shell IDE

‫👉 במסוף Google Cloud, עוברים אל Gemini Code Assist Tools (הכלים של Gemini Code Assist), מסכימים לתנאים ולהגבלות ומפעילים את Gemini Code Assist ללא עלות.

01-04-code-assist-enable.png

מתעלמים מהגדרת ההרשאות ויוצאים מהדף.

עבודה ב-Cloud Shell Editor

‫👈 לוחצים על Activate Cloud Shell (הפעלת Cloud Shell) בחלק העליון של מסוף Google Cloud (זהו סמל בצורת טרמינל בחלק העליון של חלונית Cloud Shell), ואז לוחצים על הלחצן Open Editor (פתיחת העורך) (הוא נראה כמו תיקייה פתוחה עם עיפרון). ייפתח חלון עם Cloud Shell Code Editor. בצד ימין יופיע סייר הקבצים.

Cloud Shell

‫👈 לוחצים על הלחצן Cloud Code Sign-in (כניסה באמצעות קוד בענן) בשורת הסטטוס התחתונה, כמו שמוצג. נותנים הרשאה לפלאגין לפי ההוראות. אם בשורת הסטטוס מופיע Cloud Code - no project, בוחרים באפשרות הזו, ואז בתפריט הנפתח 'Select a Google Cloud 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 API הנדרשים:

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"

‫👈אימות התוצאה במסוף IAMמסוף IAM

‫👉 מריצים את הפקודות הבאות בטרמינל כדי ליצור מכונת 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 של ספר בקטגוריה הזו. לאחר מכן הוא מציג את המלצות הספרים האלה כנקודת קצה של API ל-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 יכול להיות כלי מצוין ליצירת אב טיפוס מהיר וליצירת מבני קוד ראשוניים, וכדאי להשתמש בו כדי לקבל יתרון התחלתי.

מכיוון שמדובר בסדנה, נמשיך עם הקוד המאומת שמופיע בהמשך. עם זאת, אתם יכולים להתנסות בקוד שנוצר על ידי 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

‫👈 מגדירים את Function entry point: recommended

03-02-function-create.png

‫👈 לוחצים על שמירה ופריסה (או על שמירה ופריסה מחדש) כדי לפרוס את הפונקציה. ממתינים לסיום תהליך הפריסה. הסטטוס יוצג במסוף Cloud. התהליך עשוי להימשך כמה דקות.

טקסט חלופי 👉אחרי הפריסה, חוזרים אל Cloud Shell Editor, מריצים את הפקודה הבאה בטרמינל:

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 Project (פרויקט שלד של Bootstrap). מוודאים שאתם ב-Cloud Shell Editor. במהלך הפעלת הטרמינל,

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

אחרי הרצת הפקודה הזו, תיקייה חדשה בשם aidemy-bootstrap תיווצר בסביבת Cloud Shell.

בחלונית Explorer של Cloud Shell Editor (בדרך כלל בצד ימין), אמורה להופיע עכשיו התיקייה שנוצרה כששיכפלתם את מאגר Git‏ aidemy-bootstrap. פותחים את תיקיית השורש של הפרויקט בסייר. תופיע תיקיית משנה planner, צריך לפתוח גם אותה. סייר הפרויקטים

נתחיל ליצור את הכלים שהסוכנים שלנו ישתמשו בהם כדי להיות באמת מועילים. כמו שאתם יודעים, מודלים גדולים של שפה (LLM) מצטיינים בנימוקים וביצירת טקסט, אבל הם צריכים גישה למקורות חיצוניים כדי לבצע משימות בעולם האמיתי ולספק מידע מדויק ועדכני. אפשר לחשוב על הכלים האלה כעל "הסכין השווייצרית" של הסוכן, שמאפשרת לו לקיים אינטראקציה עם העולם.

כשמפתחים סוכן, קל מאוד לקודד הרבה פרטים. כך נוצר סוכן לא גמיש. במקום זאת, באמצעות יצירה ושימוש בכלים, לסוכן יש גישה ללוגיקה או למערכות חיצוניות, מה שמעניק לו את היתרונות של מודל שפה גדול ושל תכנות מסורתי.

בקטע הזה ניצור את הבסיס לסוכן התכנון, שהמורים ישתמשו בו כדי ליצור תוכניות לימודים. לפני שהסוכן יתחיל ליצור תוכנית, נגדיר גבולות על ידי מתן פרטים נוספים על הנושא. אנחנו ניצור שלושה כלים:

  1. קריאה ל-Restful API: אינטראקציה עם API קיים כדי לאחזר נתונים.
  2. שאילתת מסד נתונים: אחזור נתונים מובְנים ממסד נתונים של Cloud SQL.
  3. חיפוש Google: גישה למידע בזמן אמת מהאינטרנט.

אחזור המלצות על ספרים מ-API

קודם כל, ניצור כלי לאחזור המלצות לספרים מ-API של ספק ספרים שפרסנו בקטע הקודם. הדוגמה הזו ממחישה איך סוכן יכול להשתמש בשירותים קיימים.

המלצה על ספר

ב-Cloud Shell Editor, פותחים את הפרויקט 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): הפונקציה הזו מקבלת שאילתת משתמש כקלט.
  • אינטראקציה עם LLM: המערכת משתמשת ב-LLM כדי לחלץ את הקטגוריה מהשאילתה. הדוגמה הזו מדגימה איך אפשר להשתמש ב-LLM כדי ליצור פרמטרים לכלים.
  • קריאה ל-API: הפונקציה שולחת בקשת POST ל-API של ספק הספרים, ומעבירה את הקטגוריה ואת מספר הספרים הרצוי.

‫👈 כדי לבדוק את הפונקציה החדשה, מגדירים את משתנה הסביבה ומריצים את הפקודה :

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 שמכילה המלצות לספרים שאוחזרו מה-API של ספק הספרים. התוצאות נוצרות באופן אקראי. יכול להיות שהספרים שלכם יהיו שונים, אבל אתם אמורים לקבל שתי המלצות על ספרים בפורמט 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"}]

אם זה מה שמופיע, סימן שהכלי הראשון פועל בצורה תקינה.

במקום ליצור באופן מפורש קריאה ל-API מסוג REST עם פרמטרים ספציפיים, אנחנו משתמשים בשפה טבעית ("אני משתתף בקורס..."). הסוכן מחלץ בצורה חכמה את הפרמטרים הנדרשים (כמו הקטגוריה) באמצעות NLP, וכך מדגיש איך הסוכן ממנף את ההבנה של שפה טבעית כדי לקיים אינטראקציה עם ה-API.

השוואת שיחות

‫👈Remove the following testing code from the 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. כך הסוכן יכול לגשת למקור מידע אמין לתכנון שיעורים.

יצירת מסד נתונים

זוכרים את מופע 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 בתור user ו-1234qwer בתור password.

לוחצים על אימות.

כניסה ל-SQL Studio

‫👈 בעורך השאילתות של SQL Studio, עוברים לכרטיסייה Editor 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 ומכניס לתוכה נתונים לדוגמה.

‫👈 לוחצים על Run כדי להריץ את קוד ה-SQL. אמורה להופיע הודעת אישור שההצהרות בוצעו בהצלחה.

👈 מרחיבים את הכלי לבדיקת נתונים, מאתרים את הטבלה החדשה curriculums ולוחצים על שאילתה. תיפתח כרטיסייה חדשה של עורך עם SQL שנוצר בשבילכם,

sql studio select table

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

‫👈 לוחצים על הפעלה.

בטבלת התוצאות אמורות להופיע שורות הנתונים שהוספתם בשלב הקודם, כדי לוודא שהטבלה והנתונים נוצרו בצורה נכונה.

אחרי שיצרתם מסד נתונים עם נתוני תוכנית לימודים לדוגמה, נבנה כלי לאחזור הנתונים.

‫👈 בעורך Cloud Code, עורכים את הקובץ 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 כדי לעצור אותו.

‫👈Remove the following testing code from the curriculums.py

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

‫👈כדי לצאת מהסביבה הווירטואלית, מריצים את הפקודה הבאה במסוף:

deactivate

6. כלי בנייה: גישה למידע בזמן אמת מהאינטרנט

לבסוף, נבנה כלי שמשתמש בשילוב של Gemini 2 וחיפוש Google כדי לגשת למידע בזמן אמת מהאינטרנט. כך הסוכן יכול להתעדכן ולספק תוצאות רלוונטיות.

השילוב של Gemini 2 עם Google Search API משפר את יכולות הסוכן על ידי מתן תוצאות חיפוש מדויקות ורלוונטיות יותר מבחינת ההקשר. כך הסוכנים יכולים לגשת למידע עדכני ולעגן את התשובות שלהם על נתונים מהעולם האמיתי, ולמזער את ההזיות. שילוב ה-API המשופר גם מאפשר שאילתות בשפה טבעית, ומאפשר לנציגים לנסח בקשות חיפוש מורכבות ומדויקות.

חיפוש

הפונקציה הזו מקבלת כקלט שאילתת חיפוש, תוכנית לימודים, נושא ושנה, ומשתמשת ב-Gemini API ובכלי חיפוש Google כדי לאחזר מידע רלוונטי מהאינטרנט. אם תתבוננו מקרוב, תראו שהיא משתמשת ב-SDK של Google Generative AI כדי לבצע קריאה לפונקציה בלי להשתמש במסגרת אחרת.

‫👈עורכים את 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 מנתח באופן פנימי את search_text וקובע אם הוא יכול לענות על השאלה ישירות או שהוא צריך להשתמש בכלי GoogleSearch. זהו שלב קריטי שמתרחש בתהליך ההיגיון של מודל ה-LLM. המודל אומן לזהות מצבים שבהם נדרשים כלים חיצוניים. אם המודל מחליט להשתמש בכלי GoogleSearch, ‏ Google Generative AI SDK מטפל בהפעלה בפועל. ערכת ה-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 עם תוצאות חיפוש שקשורות ל'סילבוס למתמטיקה לשנה 5'. הפלט המדויק תלוי בתוצאות החיפוש, אבל הוא יהיה אובייקט 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

מעולה! יצרתם עכשיו שלושה כלים עוצמתיים לסוכן התכנון שלכם: מחבר API, מחבר מסד נתונים וכלי לחיפוש Google. הכלים האלה יאפשרו לסוכן לגשת למידע וליכולות שהוא צריך כדי ליצור תוכניות לימודים יעילות.

7. תזמור באמצעות LangGraph

אחרי שבנינו את הכלים הנפרדים, הגיע הזמן לתזמן אותם באמצעות LangGraph. כך נוכל ליצור סוכן 'מתכנן' מתוחכם יותר שיכול להחליט בצורה חכמה באילו כלים להשתמש ומתי, על סמך בקשת המשתמש.

‫LangGraph היא ספריית Python שנועדה להקל על בניית אפליקציות מרובות משתתפים עם מצב (stateful) באמצעות מודלים גדולים של שפה (LLM). אפשר לחשוב על זה כעל מסגרת לניהול שיחות מורכבות ותהליכי עבודה שכוללים מודלים גדולים של שפה (LLM), כלים וסוכנים אחרים.

מושגים מרכזיים:

  • מבנה הגרף: LangGraph מייצג את הלוגיקה של האפליקציה כגרף מכוון. כל צומת בתרשים מייצג שלב בתהליך (לדוגמה, קריאה למודל שפה גדול, הפעלה של כלי או בדיקה מותנית). קצוות מגדירים את זרימת ההפעלה בין הצמתים.
  • מצב:‏ LangGraph מנהל את המצב של האפליקציה בזמן שהיא עוברת בתרשים. המצב הזה יכול לכלול משתנים כמו הקלט של המשתמש, התוצאות של קריאות לכלים, פלטים ביניים מ-LLM וכל מידע אחר שצריך לשמור בין השלבים.
  • צמתים: כל צומת מייצג חישוב או אינטראקציה. הם יכולים להיות:
    • צמתי כלים: שימוש בכלי (לדוגמה, ביצוע חיפוש באינטרנט, שליחת שאילתה למסד נתונים)
    • צמתי פונקציה: הפעלה של פונקציית 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"])} 

הפונקציה הזו אחראית לקבל את המצב הנוכחי של השיחה, לספק ל-LLM הודעת מערכת ואז לבקש מ-LLM ליצור תגובה. מודל ה-LLM יכול להשיב ישירות למשתמש או לבחור להשתמש באחד מהכלים הזמינים.

tools : הרשימה הזו מייצגת את קבוצת הכלים שהסוכן יכול להשתמש בהם. הוא מכיל שלוש פונקציות של כלי שהגדרנו בשלבים הקודמים: get_curriculum,‏ search_latest_resource ו-recommend_book. ‫llm.bind_tools(tools): הפונקציה 'קושרת' את רשימת הכלים לאובייקט llm. הקישור של הכלים אומר למודל שפה גדול שהכלים האלה זמינים, ומספק למודל מידע על אופן השימוש בהם (למשל, שמות הכלים, הפרמטרים שהם מקבלים והפעולות שהם מבצעים).

נשתמש ב-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. קשתות מותנות מאפשרות לגרף להתפצל על סמך ההחלטה של מודל ה-LLM לגבי הכלי שבו צריך להשתמש (או אם להשיב למשתמש ישירות). כאן נכנסת לתמונה ה"אינטליגנציה" של הסוכן – הוא יכול להתאים את ההתנהגות שלו באופן דינמי בהתאם למצב.
  • לולאה: מוסיף קצה לגרף שמחבר את הצומת "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. כך המורים יוכלו לתקשר עם הסוכן באמצעות ממשק מוכר שמבוסס על טופס. אינטראקציות עם צ'אטבוטים הן דבר נפוץ במודלים מסוג LLM, אבל אנחנו בוחרים בממשק משתמש מסורתי של שליחת טופס, כי הוא עשוי להיות אינטואיטיבי יותר עבור הרבה אנשי חינוך.

‫👈 אם סגרתם את הטרמינל או שמשתני הסביבה לא מוגדרים יותר, מריצים מחדש את הפקודות הבאות

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 בשדה Year (שנה), בוחרים את הנושא Mathematics ומקלידים Geometry בשדה Add-on Request (בקשה לתוסף).

‫👈 אם ניווטתם אל מחוץ לממשק המשתמש של האפליקציה, צריך לחזור אליו ואז תראו את הפלט שנוצר.

‫👈 במסוף, עוצרים את הסקריפט על ידי הקשה על Ctrl+C.

‫👈 בטרמינל, יוצאים מהסביבה הווירטואלית:

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. התמונה aidemy-planner אמורה להופיע במאגר agent-repository. תמונה של Aidemy planner

אבטחת פרטי כניסה למסד נתונים באמצעות Secret Manager

כדי לנהל את פרטי הכניסה למסד הנתונים ולגשת אליהם בצורה מאובטחת, נשתמש ב-Secret Manager של Google Cloud. כך נמנעת קידוד קשיח של מידע רגיש בקוד האפליקציה שלנו, והאבטחה משתפרת.

המערכת תיצור סודות נפרדים לשם המשתמש, לסיסמה ולשם של מסד הנתונים. הגישה הזו מאפשרת לנו לנהל כל אישור בנפרד.

‫👈 בטרמינל מריצים את הפקודה הבאה:

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 היא פלטפורמה מנוהלת ללא שרת (serverless) שמאפשרת לפרוס אפליקציות בקונטיינרים במהירות ובקלות. היא מסתירה את ניהול התשתית, ומאפשרת לכם להתמקד בכתיבה ובפריסה של הקוד. נפרוס את הכלי לתכנון כ-Cloud Run service.

‫👈 במסוף Google Cloud, עוברים אל Cloud Run. לוחצים על DEPLOY CONTAINER (פריסת מאגר) ובוחרים באפשרות 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. מרחיבים את הקטע Container(s), Volumes, Networking, Security (מאגרי נתונים, אמצעי אחסון, רשת, אבטחה) ומגדירים את האפשרויות הבאות בכרטיסייה Container(s) (:
    • כרטיסיית ההגדרות:
      • מקורות מידע
        • זיכרון : 2GiB
    • הכרטיסייה Variables & Secrets (משתנים וסודות):
      • משתני סביבה, מוסיפים את המשתנים הבאים בלחיצה על הלחצן + הוספת משתנה:
        • מוסיפים שם: 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)"
          
      • בקטע Secrets exposed as environment variables (סודות שמוצגים כמשתני סביבה), מוסיפים את הסודות הבאים באמצעות הלחצן + Reference as a secret (הפניה כסוד):
        • מוסיפים שם: DB_USER, סוד: בוחרים באפשרות db-user וגרסה:latest
        • מוסיפים שם: DB_PASS, סוד: בוחרים באפשרות db-pass וגרסה:latest
        • מוסיפים שם: DB_NAME, סוד: בוחרים באפשרות db-name וגרסה:latest

הגדרת סוד

משאירים את שאר הערכים כברירת מחדל.

‫👈 לוחצים על יצירה.

השירות ייפרס ב-Cloud Run.

אחרי הפריסה, אם אתם לא נמצאים כבר בדף הפרטים, לוחצים על שם השירות כדי לעבור לדף הפרטים שלו. כתובת ה-URL של הפריסה מופיעה בחלק העליון.

כתובת URL

‫👈 בממשק של האפליקציה, בוחרים 7 בשדה Year (שנה), בוחרים Mathematics בשדה Subject (נושא) ומזינים Algebra בשדה Add-on Request (בקשה להוספת תוסף).

‫👈 לוחצים על יצירת תוכנית. כך הסוכן יקבל את ההקשר הנדרש כדי ליצור תוכנית לימודים מותאמת.

מעולה! יצרת בהצלחה תוכנית לימודים באמצעות סוכן ה-AI המתקדם שלנו. הדוגמה הזו ממחישה את הפוטנציאל של סוכנים להפחית באופן משמעותי את עומס העבודה ולייעל את המשימות, ובסופו של דבר לשפר את היעילות ולהקל על אנשי החינוך.

9. מערכות מרובות סוכנים

אחרי שהטמענו בהצלחה את הכלי ליצירת תוכנית לימודים, נתמקד עכשיו בבניית פורטל התלמידים. הפורטל הזה יספק לתלמידים גישה לחידונים, לסיכומי אודיו ולמטלות שקשורות לעבודת הקורס שלהם. בהתחשב בהיקף הפונקציונליות הזו, נשתמש בעוצמה של מערכות מרובות סוכנים כדי ליצור פתרון מודולרי וניתן להרחבה.

כפי שציינו קודם, במקום להסתמך על סוכן יחיד שיטפל בכל דבר, מערכת מרובת סוכנים מאפשרת לנו לחלק את עומס העבודה למשימות קטנות יותר וספציפיות יותר, שכל אחת מהן מטופלת על ידי סוכן ייעודי. לגישה הזו יש כמה יתרונות מרכזיים:

מודולריות ויכולת תחזוקה: במקום ליצור סוכן יחיד שמבצע את כל הפעולות, כדאי לבנות סוכנים קטנים יותר וייעודיים עם תחומי אחריות מוגדרים היטב. המודולריות הזו מקלה על ההבנה, התחזוקה והניפוי של הבאגים במערכת. כשמתעוררת בעיה, אפשר לבודד אותה לסוכן ספציפי, במקום לעבור על בסיס קוד עצום.

יכולת הרחבה: הרחבת סוכן יחיד ומורכב עלולה ליצור צוואר בקבוק. בעזרת מערכת מרובת סוכנים, אפשר להרחיב את השימוש בסוכנים ספציפיים בהתאם לצרכים שלהם. לדוגמה, אם סוכן אחד מטפל במספר גדול של בקשות, אפשר להפעיל בקלות עוד מופעים של הסוכן הזה בלי להשפיע על שאר המערכת.

התמחות הצוות: אפשר לחשוב על זה כך: לא מבקשים ממהנדס אחד לבנות אפליקציה שלמה מאפס. במקום זאת, אתם מרכיבים צוות של מומחים, שלכל אחד מהם יש מומחיות בתחום מסוים. באופן דומה, מערכת מרובת סוכנים מאפשרת לכם להשתמש ביתרונות של מודלים שונים של LLM וכלים שונים, ולהקצות אותם לסוכנים שהכי מתאימים למשימות ספציפיות.

פיתוח מקביל: צוותים שונים יכולים לעבוד על סוכנים שונים בו-זמנית, וכך לקצר את תהליך הפיתוח. מכיוון שהסוכנים הם עצמאיים, שינויים בסוכן אחד לא צפויים להשפיע על סוכנים אחרים.

ארכיטקטורה מבוססת-אירועים

כדי לאפשר תקשורת ותיאום יעילים בין הסוכנים האלה, נשתמש בארכיטקטורה מבוססת-אירועים. המשמעות היא שהסוכנים יגיבו ל'אירועים' שמתרחשים במערכת.

הסוכנים נרשמים לסוגים ספציפיים של אירועים (למשל, "תוכנית הוראה נוצרה", "מטלה נוצרה"). כשמתרחש אירוע, הסוכנים הרלוונטיים מקבלים על כך הודעה ויכולים להגיב בהתאם. ההפרדה הזו מאפשרת גמישות, יכולת הרחבה ורספונסיביות בזמן אמת.

סקירה כללית

עכשיו, כדי להתחיל, אנחנו צריכים דרך לשדר את האירועים האלה. כדי לעשות את זה, נגדיר נושא Pub/Sub. נתחיל ביצירת נושא בשם תוכנית.

‫👈 עוברים אל Pub/Sub במסוף Google Cloud.

‫👈 לוחצים על הלחצן יצירת נושא.

‫👉 מגדירים את הנושא עם מזהה/שם plan ומבטלים את הסימוןAdd a default subscription, משאירים את שאר ההגדרות כברירת מחדל ולוחצים על יצירה.

הדף Pub/Sub יתרענן, ועכשיו הנושא החדש שיצרתם אמור להופיע בטבלה. יצירת נושא

עכשיו נשלב את הפונקציונליות של פרסום אירועים ב-Pub/Sub בסוכן התכנון שלנו. נוסיף כלי חדש שישלח אירוע מסוג plan לנושא Pub/Sub שיצרנו. האירוע הזה יאותת לסוכנים אחרים במערכת (כמו אלה בפורטל התלמידים) שיש תוכנית לימודים חדשה.

‫👈 חוזרים אל Cloud Code Editor ופותחים את הקובץ 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 publisher, בונה את נתיב הנושא, ממירה את תוכנית ההוראה למחרוזת JSON ומפרסמת את ההודעה בנושא.

באותו קובץ app.py

‫👈 מעדכנים את ההנחיה כדי להורות לנציג לשלוח את אירוע תוכנית הלימודים לנושא Pub/Sub אחרי יצירת תוכנית הלימודים. *החלפה

### ADD send_plan_event CALL

עם הפרטים הבאים:

send_plan_event(teaching_plan)

הוספנו את הכלי send_plan_event ושינינו את ההנחיה, וכך אפשרנו לסוכן המתכנן שלנו לפרסם אירועים ב-Pub/Sub. כך רכיבים אחרים במערכת שלנו יכולים להגיב ליצירה של תוכניות לימוד חדשות. עכשיו יש לנו מערכת רב-סוכנים פונקציונלית, כפי שמוסבר בקטעים הבאים.

10. העצמת התלמידים באמצעות בחנים על פי דרישה

תארו לעצמכם סביבת למידה שבה לתלמידים יש גישה לאספקה אינסופית של חידונים שמותאמים לתוכניות הלמידה הספציפיות שלהם. החידונים האלה מספקים משוב מיידי, כולל תשובות והסברים, ומעודדים הבנה מעמיקה יותר של החומר. זה הפוטנציאל שאנחנו רוצים לממש באמצעות פורטל החידונים מבוסס ה-AI שלנו.

כדי להגשים את החזון הזה, נבנה רכיב ליצירת בחנים שיכול ליצור שאלות אמריקאיות על סמך התוכן של תוכנית הלימודים.

סקירה כללית

‫👈 בחלונית 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 שנועד במיוחד להבין את הפלט של ה-LLM ולבנות אותו. הוא משתמש במודל 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 יפתח כרטיסייה חדשה או חלון חדש בדפדפן עם תצוגה מקדימה של האפליקציה באינטרנט.

‫👈 באפליקציית האינטרנט, לוחצים על הקישור 'בחנים' בסרגל הניווט העליון או בכרטיס בדף האינדקס. אמורות להופיע לתלמיד/ה שלוש חידונים שנוצרו באופן אקראי. החידונים האלה מבוססים על תוכנית הלימודים ומדגימים את היכולות של מערכת יצירת החידונים שלנו שמבוססת על AI.

חידונים

‫👈 כדי להפסיק את התהליך שפועל באופן מקומי, לוחצים על Ctrl+C במסוף.

‫Gemini 2 חושב על הסברים

אוקיי, יש לנו בחנים, וזה מצוין! אבל מה קורה אם התלמידים טועים? שם מתרחשת הלמידה האמיתית, נכון? אם נסביר להם למה התשובה שלהם לא נכונה ואיך מגיעים לתשובה הנכונה, סביר יותר שהם יזכרו אותה. בנוסף, זה עוזר להם להבין את התהליך ולבטוח בו.

לכן אנחנו הולכים להשתמש בטכנולוגיה המתקדמת ביותר: מודל ה'חשיבה' של Gemini 2! אפשר לחשוב על זה כאילו נותנים ל-AI עוד קצת זמן לחשוב לפני שהוא מסביר. כך הוא יכול לתת משוב מפורט וטוב יותר.

אנחנו רוצים לבדוק אם הוא יכול לעזור לתלמידים באמצעות סיוע, מענה והסברים מפורטים. כדי לבדוק את זה, נתחיל בנושא מסובך במיוחד – חשבון דיפרנציאלי ואינטגרלי.

סקירה כללית

‫👈 קודם כל, עוברים אל 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.

הפעולה הזו מחליפה את מודל ה-LLM במודל אחר שמתאים יותר לניתוח מידע. כך המודל יוכל ליצור הסברים טובים יותר.

‫👉 מריצים את הסקריפט 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.

‫👈REMOVE the following test code from 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 יפתח כרטיסייה חדשה או חלון חדש בדפדפן עם תצוגה מקדימה של האפליקציה באינטרנט.

‫👈 באפליקציית האינטרנט, לוחצים על הקישור 'בחנים' בסרגל הניווט העליון או בכרטיס בדף האינדקס.

‫👉 עונים על כל החידונים ומוודאים שלפחות תשובה אחת לא נכונה, ואז לוחצים על שליחה.

תשובות שנוצרו על ידי AI

במקום להמתין לתגובה בלי לעשות כלום, אפשר לעבור לטרמינל של 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. כך תוכלו לוודא שהם כוללים את הקוד העדכני ביותר ומוגדרים לתקשורת באמצעות אירועים.

סקירה כללית על פריסה

‫👈 קודם נבנה מחדש את תמונת הסוכן planner ונדחוף אותה. כדי לעשות את זה, מריצים את הפקודה הבאה במסוף:

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.
  • חשבון שירות:
    • מקצים לחשבון השירות את התפקיד roles/iam.serviceAccountTokenCreator
    • שימוש בערך ברירת המחדל: חשבון שירות ברירת המחדל של Compute
  • יעד האירוע: Cloud Run
  • שירות Cloud Run: aidemy-portal
  • התעלמות מהודעת השגיאה: ההרשאה נדחתה ב-'locations/me-central2' (או שהוא לא קיים).
  • נתיב כתובת ה-URL של השירות: /new_teaching_plan

‫👈 לוחצים על 'יצירה'.

הדף Eventarc Triggers יתרענן, ועכשיו הטריגר החדש שיצרתם אמור להופיע בטבלה.

עכשיו אפשר לגשת לסוכן המתכנן באמצעות כתובת ה-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, תוך שימוש בארכיטקטורה מבוססת-אירועים לשיפור המדרגיות והגמישות. הנחתם תשתית מוצקה, אבל יש עוד הרבה מה ללמוד. כדי להעמיק ביתרונות האמיתיים של הארכיטקטורה הזו, לגלות את העוצמה של Gemini 2 API במצב Live עם תמיכה בריבוי מודאלי, וללמוד איך להטמיע תזמור נתיב יחיד באמצעות LangGraph, אפשר להמשיך לשני הפרקים הבאים.

12. אופציונלי: סיכום של השנה שהייתה באמצעות Gemini

‫Gemini יכול להבין ולעבד מידע ממקורות שונים, כמו טקסט, תמונות ואפילו אודיו, וכך נפתחות אפשרויות חדשות ללמידה וליצירת תוכן. היכולת של Gemini 'לראות', 'לשמוע' ו'לקרוא' מאפשרת ליצור חוויות משתמש יצירתיות ומרתקות.

מעבר ליצירת רכיבים חזותיים או טקסט, שלב חשוב נוסף בתהליך הלמידה הוא סיכום יעיל של החומר. תחשבו על זה: כמה פעמים קרה לכם שזכרתם בקלות מילים של שיר קליט יותר מאשר משהו שקראתם בספר לימוד? צלילים יכולים להיות בלתי נשכחים! לכן, אנחנו מתכוונים להשתמש ביכולות המולטי-מודאליות של Gemini כדי ליצור סיכומים קוליים של תוכניות הלימוד שלנו. כך התלמידים יוכלו לעיין בחומר בצורה נוחה ומרתקת, וזה עשוי לשפר את יכולת השמירה וההבנה שלהם באמצעות למידה שמתבססת על שמיעה.

סקירה כללית על Live API

אנחנו צריכים מקום לאחסון קובצי האודיו שנוצרו. ‫Cloud Storage מספק פתרון ניתן להתאמה ואמין.

‫👈 נכנסים אל Storage במסוף. בתפריט הימני, לוחצים על 'מאגרי נתונים'. לוחצים על הלחצן '+ יצירה' בחלק העליון.

‫👈מגדירים את הקטגוריה החדשה:

  • שם הקטגוריה: aidemy-recap-UNIQUE_NAME.
    • חשוב: צריך להגדיר שם ייחודי לקטגוריה שמתחיל ב-aidemy-recap-. התחילית הייחודית הזו חשובה מאוד כדי למנוע התנגשויות בשמות כשיוצרים את הקטגוריה ב-Cloud Storage.
  • אזור: us-central1.
  • סוג האחסון: Standard. ‫Standard מתאים לנתונים שניגשים אליהם לעיתים קרובות.
  • בקרת גישה: משאירים את ברירת המחדל 'אחידה' לבקרת הגישה. כך מתקבלת בקרת גישה עקבית ברמת הקטגוריה.
  • אפשרויות מתקדמות: בדרך כלל הגדרות ברירת המחדל מספיקות לסדנה הזו.

לוחצים על הלחצן CREATE כדי ליצור את הקטגוריה.

  • יכול להיות שיופיע חלון קופץ לגבי מניעת גישה ציבורית. משאירים את התיבה 'אכיפה של מניעת גישה ציבורית בקטגוריה הזו' מסומנת ולוחצים על Confirm.

עכשיו הקטגוריה החדשה שיצרתם תופיע ברשימת הקטגוריות. חשוב לזכור את שם הקטגוריה, כי תצטרכו להשתמש בו בהמשך.

‫👈 בטרמינל של Cloud Code Editor, מריצים את הפקודות הבאות כדי להעניק לחשבון השירות גישה לקטגוריה:

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 Code Editor, פותחים את 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. בניגוד לקריאה רגילה ל-API שבה שולחים בקשה ומקבלים תגובה, החיבור הזה נשאר פתוח להחלפת נתונים רציפה.
  • הגדרה רב-אופנית: אפשר להשתמש בהגדרה כדי לציין את סוג הפלט הרצוי (במקרה הזה, אודיו), ואפילו לציין את הפרמטרים שרוצים להשתמש בהם (למשל, בחירת קול, קידוד אודיו)
  • עיבוד אסינכרוני: ה-API הזה פועל באופן אסינכרוני, כלומר הוא לא חוסם את ה-thread הראשי בזמן ההמתנה לסיום יצירת האודיו. הוא מעבד את הנתונים בזמן אמת ושולח את הפלט במנות, וכך מספק חוויה כמעט מיידית.

עכשיו, השאלה המרכזית היא: מתי צריך להפעיל את תהליך יצירת האודיו הזה? הכי טוב שהסיכומים הקוליים יהיו זמינים ברגע שיוצרים תוכנית לימודים חדשה. מכיוון שכבר הטמענו ארכיטקטורה מבוססת-אירועים על ידי פרסום תוכנית הלימודים בנושא Pub/Sub, אנחנו יכולים פשוט להירשם לנושא הזה.

עם זאת, אנחנו לא יוצרים תוכניות לימודים חדשות לעיתים קרובות. לא יעיל להפעיל סוכן כל הזמן ולהמתין לתוכניות חדשות. לכן, הגיוני מאוד לפרוס את הלוגיקה של יצירת האודיו הזו כפונקציית Cloud Run.

אם פורסים את הפונקציה, היא נשארת במצב לא פעיל עד שמתפרסמת הודעה חדשה בנושא Pub/Sub. במקרה כזה, הפונקציה מופעלת אוטומטית, יוצרת את סיכומי האודיו ושומרת אותם בקטגוריה שלנו.

‫👈בתיקייה 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 מאפשר לנו לבדוק את הפונקציה באופן מקומי לפני הפריסה שלה ב-Google Cloud. מפעילים אמולטור מקומי באמצעות הפקודה:

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

‫👈 בזמן שהאמולטור פועל, אפשר לשלוח אליו CloudEvents כדי לדמות פרסום של תוכנית לימודים חדשה. בטרמינל חדש:

שני טרמינלים

‫👉Run:

  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, בוחרים בכרטיסייה Bucket ואז באפשרות 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.אמור להופיע שירות חדש בשם courses-agent.

רשימת Cloud Run

כדי לבדוק את הגדרת הטריגר, לוחצים על השירות courses-agent כדי לראות את הפרטים שלו. עוברים לכרטיסייה TRIGGERS (טריגרים).

אמור להופיע טריגר שהוגדר להאזין להודעות שפורסמו בנושא התוכנית.

טריגר של 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

אחרי יצירת התוכנית החדשה, צריך להמתין 2-3 דקות עד ליצירת האודיו. גם הפעם, התהליך ייקח כמה דקות בגלל מגבלות החיוב בחשבון המעבדה הזה.

כדי לבדוק אם הפונקציה courses-agent קיבלה את תוכנית ההוראה, אפשר לעיין בכרטיסייה TRIGGERS (טריגרים) של הפונקציה. רעננו את הדף מדי פעם. בסופו של דבר תראו שהפונקציה הופעלה. אם הפונקציה לא הופעלה אחרי יותר מ-2 דקות, אפשר לנסות ליצור שוב את תוכנית הלימודים. עם זאת, מומלץ להימנע מיצירת תוכניות שוב ושוב בזו אחר זו, כי כל תוכנית שנוצרת נצרכת ומעובדת על ידי הסוכן באופן רציף, ועלולה ליצור עומס.

Trigger Observe

‫👉 נכנסים לפורטל ולוחצים על 'קורסים'. אמורים להופיע שלושה כרטיסים, שבכל אחד מהם מוצג סיכום אודיו. כדי למצוא את כתובת ה-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 Assignment Generator

פלטפורמות AI מבוססות-ענן הן נוחות, אבל אירוח עצמי של מודלים מסוג LLM יכול להיות חיוני להגנה על פרטיות הנתונים ולהבטחת ריבונות הנתונים. נפרוס את המודל הכי קטן של DeepSeek (1.5 מיליארד פרמטרים) במכונה של Cloud Compute Engine. יש דרכים אחרות, כמו אירוח בפלטפורמת Vertex AI של Google או אירוח במופע GKE שלכם, אבל מכיוון שזה רק סדנה בנושא סוכני AI, ואני לא רוצה להשאיר אתכם כאן לנצח, נשתמש בדרך הפשוטה ביותר. אבל אם אתם מעוניינים בכך ורוצים לבדוק אפשרויות אחרות, תוכלו לעיין בקובץ deepseek-vertexai.py בתיקיית המטלות, שבו מופיע קוד לדוגמה שמראה איך ליצור אינטראקציה עם מודלים שפרוסים ב-VertexAI.

סקירה כללית על 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 פועלת:

ב-Google Cloud Console, עוברים אל Compute Engine > ‏VM instances (מכונות VM). האפליקציה ollama-instance אמורה להופיע עם סימן וי ירוק שמציין שהיא פועלת. אם לא רואים את האפשרות הזו, צריך לוודא שהאזור הוא us-central1. אם היא לא מופיעה, יכול להיות שתצטרכו לחפש אותה.

רשימת Compute Engine

‫👈 נתקין את המודל הכי קטן של DeepSeek ונבדוק אותו. בטרמינל New ב-Cloud Shell Editor, מריצים את הפקודה הבאה כדי להתחבר למופע GCE באמצעות SSH.

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

אחרי שיוצרים את חיבור ה-SSH, יכול להיות שתוצג לכם ההודעה הבאה:

"Do you want to continue (Y/n)?" (רוצה להמשיך (Y/n)?).

פשוט מקלידים Y(לא משנה אם האותיות קטנות או גדולות) ומקישים על Enter כדי להמשיך.

לאחר מכן, יכול להיות שתתבקשו ליצור ביטוי סיסמה למפתח ה-SSH. אם לא רוצים להשתמש בביטוי גישה, פשוט מקישים פעמיים על Enter כדי לאשר את ברירת המחדל (ללא ביטוי גישה).

‫👈עכשיו אתם במכונה הווירטואלית. אפשר למשוך את המודל הקטן ביותר של DeepSeek R1 ולבדוק אם הוא פועל.

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

‫👈יוצאים ממופע GCE ומזינים את הפקודה הבאה בטרמינל של SSH:

exit

‫👉 לאחר מכן, מגדירים את מדיניות הרשת כדי ששירותים אחרים יוכלו לגשת ל-LLM. אם רוצים לעשות את זה בסביבת ייצור, צריך להגביל את הגישה למופע, להטמיע כניסה מאובטחת לשירות או להגביל את הגישה לפי כתובת 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 מסנתז את שתי המטלות האלה למטלה מקיפה אחת. מכיוון שאנחנו מגדירים מראש את רצף השלבים בלי קבלת החלטות של LLM, זהו תהליך תזמור חד-נתיבי שהוגדר על ידי המשתמש.

סקירה כללית של Langraph combine

‫👈מדביקים את הקוד הבא בסוף הקובץ 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 באופן מקומי. כך אפשר לבצע איטרציות מהר יותר ולנפות באגים בקלות רבה יותר.

קודם צריך ליצור קטגוריה ב-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 עשוי להופיע באותה שורה של שורת הפקודה של מעטפת הטרמינל).

כדי לוודא שההקצאה נוצרה ואוחסנה בהצלחה, נכנסים אל Storage > Cloud Storage במסוף Google Cloud. בוחרים את הקטגוריה 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 

כדי לוודא שהפריסה בוצעה, עוברים אל Cloud Run במסוף Google Cloud. אמור להופיע שירות חדש בשם courses-agent. 12-03-function-list

אחרי שהטמענו, בדקנו ופרסנו את תהליך העבודה ליצירת מטלות, אפשר לעבור לשלב הבא: מתן גישה למטלות האלה בפורטל התלמידים.

14. אופציונלי: עבודה משותפת מבוססת-תפקידים עם Gemini ו-DeepSeek – המשך

יצירה דינמית של אתרים

כדי לשפר את פורטל התלמידים ולהפוך אותו למעניין יותר, נטמיע יצירת HTML דינמית בדפי המטלות. המטרה היא לעדכן את הפורטל באופן אוטומטי בעיצוב חדש ומושך מבחינה ויזואלית בכל פעם שנוצרת מטלה חדשה. התכונה הזו משתמשת ביכולות כתיבת הקוד של ה-LLM כדי ליצור חוויית משתמש דינמית ומעניינת יותר.

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

כדי לעצור את הסקריפט, לוחצים על 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 Triggers במסוף Google Cloud. הערך portal-assignment-trigger אמור להופיע בטבלה. כדי לראות את הפרטים של הטריגר, לוחצים על שם הטריגר. טריגר להקצאת הרשאות

יכול להיות שיחלפו 2-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! רכשת ניסיון מעשי ותובנות חשובות לגבי:

  • היתרונות של מערכות מרובות סוכנים, כולל מודולריות, יכולת הרחבה, התמחות ותחזוקה פשוטה.
  • החשיבות של ארכיטקטורות מבוססות-אירועים לבניית אפליקציות רספונסיביות עם צימוד רופף.
  • שימוש אסטרטגי במודלים מסוג LLM, התאמת המודל הנכון למשימה ושילוב המודלים עם כלים להשגת השפעה בעולם האמיתי.
  • שיטות פיתוח מבוססות-ענן באמצעות שירותי Google Cloud ליצירת פתרונות ניתנים להרחבה ואמינים.
  • החשיבות של התחשבות בפרטיות הנתונים ובמודלים של אירוח עצמי כחלופה לפתרונות של ספקים.

עכשיו יש לכם בסיס מוצק לבניית אפליקציות מתוחכמות מבוססות-AI ב-Google Cloud.

15. אתגרים והשלבים הבאים

כל הכבוד על בניית מערכת מרובת סוכנים של Aidemy! יצרתם בסיס חזק לחינוך מבוסס-AI. עכשיו נבחן כמה אתגרים ושיפורים פוטנציאליים בעתיד, כדי להרחיב עוד יותר את היכולות של המודל ולתת מענה לצרכים בעולם האמיתי:

למידה אינטראקטיבית עם שאלות ותשובות בשידור חי:

  • אתגר: האם תוכלו להשתמש ב-Gemini 2 Live API כדי ליצור תכונה של שאלות ותשובות בזמן אמת לתלמידים? תארו לעצמכם כיתה וירטואלית שבה התלמידים יכולים לשאול שאלות ולקבל תשובות מיידיות מבוססות-AI.

שליחה ומתן ציונים אוטומטיים למטלות:

  • אתגר: תכנון והטמעה של מערכת שמאפשרת לתלמידים להגיש מטלות באופן דיגיטלי ולקבל עליהן ציון אוטומטי מ-AI, עם מנגנון לזיהוי העתקות ולמניעתן. האתגר הזה הוא הזדמנות מצוינת לבחון את השיטה של שליפה משופרת של דור (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