Aidemy: créer des systèmes multi-agents avec LangGraph, l'EDA et l'IA générative sur Google Cloud

1. Introduction

Bonjour ! Vous aimez l'idée d'agents, de petits assistants qui peuvent faire des choses pour vous sans que vous n'ayez à lever le petit doigt ? Parfait ! Mais soyons réalistes : un seul agent ne suffit pas toujours, en particulier lorsque vous travaillez sur des projets plus importants et plus complexes. Vous aurez probablement besoin d'une équipe entière de personnes de ce type ! C'est là qu'interviennent les systèmes multi-agents.

Les agents, lorsqu'ils sont alimentés par des LLM, offrent une flexibilité incroyable par rapport au codage en dur à l'ancienne. Mais, et il y a toujours un "mais", elles présentent des défis délicats qui leur sont propres. C'est exactement ce que nous allons découvrir dans cet atelier.

titre

Voici ce que vous allez apprendre:

Créer votre premier agent avec LangGraph: nous allons mettre les mains dans le cambouis pour créer votre propre agent à l'aide de LangGraph, un framework populaire. Vous apprendrez à créer des outils qui se connectent à des bases de données, à exploiter la dernière API Gemini 2 pour effectuer des recherches sur Internet et à optimiser les requêtes et les réponses afin que votre agent puisse interagir non seulement avec les LLM, mais aussi avec les services existants. Nous vous expliquerons également comment fonctionne l'appel de fonction.

Orchestration des agents selon vos besoins: nous allons explorer différentes façons d'orchestrer vos agents, des chemins directs simples aux scénarios multi-chemins plus complexes. Vous pouvez considérer cela comme la direction du flux de votre équipe d'agents.

Systèmes multi-agents: vous découvrirez comment configurer un système permettant à vos agents de collaborer et de travailler ensemble, grâce à une architecture basée sur les événements.

Liberté de choix des LLM : utilisez le meilleur pour la tâche : nous ne sommes pas limités à un seul LLM. Vous découvrirez comment utiliser plusieurs LLM, en leur attribuant différents rôles pour renforcer la capacité de résolution de problèmes à l'aide de "modèles de pensée" intéressants.

Contenu dynamique ? Aucun problème !: Imaginez que votre agent crée en temps réel du contenu dynamique adapté à chaque utilisateur. Nous allons vous montrer comment procéder.

Passer au cloud avec Google Cloud: ne vous contentez pas de jouer dans un bloc-notes. Nous vous montrerons comment concevoir et déployer votre système multi-agents sur Google Cloud pour qu'il soit prêt à être utilisé dans le monde réel.

Ce projet sera un bon exemple de l'utilisation de toutes les techniques dont nous avons parlé.

2. Architecture

Être enseignant ou travailler dans le secteur de l'éducation peut être très gratifiant, mais avouons-le, la charge de travail, en particulier la préparation, peut être difficile. De plus, le personnel est souvent insuffisant et les cours particuliers peuvent être coûteux. C'est pourquoi nous proposons un assistant pédagogique basé sur l'IA. Cet outil peut alléger la charge des enseignants et combler le vide créé par les pénuries de personnel et le manque de cours particuliers abordables.

Notre assistant pédagogique basé sur l'IA peut créer des plans de cours détaillés, des quiz amusants, des récapitulatifs audio faciles à suivre et des devoirs personnalisés. Les enseignants peuvent ainsi se concentrer sur ce qu'ils font le mieux: échanger avec les élèves et les aider à aimer apprendre.

Le système comporte deux sites: l'un permet aux enseignants de créer des plans de cours pour les semaines à venir,

Planificateur

et un pour les élèves afin qu'ils puissent accéder aux quiz, aux récapitulatifs audio et aux devoirs. Portail

Très bien. Je vais vous présenter l'architecture de notre assistant pédagogique, Aidemy. Comme vous pouvez le constater, nous l'avons décomposé en plusieurs composants clés, qui fonctionnent tous ensemble pour y parvenir.

Architecture

Principaux éléments et technologies d'architecture:

Google Cloud Platform (GCP): élément central de l'ensemble du système:

  • Vertex AI: accède aux LLM Gemini de Google.
  • Cloud Run: plate-forme sans serveur permettant de déployer des agents et des fonctions conteneurisés.
  • Cloud SQL: base de données PostgreSQL pour les données du programme.
  • Pub/Sub et Eventarc: base de l'architecture basée sur les événements, qui permet la communication asynchrone entre les composants.
  • Cloud Storage: stocke les retranscriptions audio et les fichiers de devoirs.
  • Secret Manager: gère de manière sécurisée les identifiants de base de données.
  • Artifact Registry: stocke les images Docker pour les agents.
  • Compute Engine: déployer un LLM autogéré au lieu de s'appuyer sur des solutions tierces

LLM : "cerveau" du système:

  • Modèles Gemini de Google: (Gemini 1.0 Pro, Gemini 2 Flash, Gemini 2 Flash Thinking, Gemini 1.5-pro) utilisés pour la planification des cours, la génération de contenu, la création dynamique d'HTML, l'explication des quiz et la combinaison des devoirs.
  • DeepSeek: utilisé pour la tâche spécialisée de génération de devoirs en auto-apprentissage

LangChain et LangGraph: frameworks pour le développement d'applications LLM

  • Facilite la création de workflows multi-agents complexes.
  • Permet d'orchestrer intelligemment les outils (appels d'API, requêtes de base de données, recherches sur le Web).
  • Implémente une architecture basée sur les événements pour assurer l'évolutivité et la flexibilité du système.

En substance, notre architecture combine la puissance des LLM avec des données structurées et une communication basée sur les événements, le tout exécuté sur Google Cloud. Nous pouvons ainsi créer un assistant pédagogique évolutif, fiable et efficace.

3. Avant de commencer

Dans la console Google Cloud, sur la page du sélecteur de projet, sélectionnez ou créez un projet Google Cloud. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier si la facturation est activée sur un projet.

👉 Cliquez sur Activer Cloud Shell en haut de la console Google Cloud (icône en forme de terminal en haut du volet Cloud Shell), puis sur le bouton "Ouvrir l'éditeur" (en forme de dossier ouvert avec un crayon). L'éditeur de code Cloud Shell s'ouvre dans la fenêtre. Un explorateur de fichiers s'affiche sur la gauche.

Cloud Shell

👉 Cliquez sur le bouton Cloud Code – Se connecter dans la barre d'état inférieure (voir ci-dessous). Autorisez le plug-in comme indiqué. Si Cloud Code – Aucun projet est affiché dans la barre d'état, cliquez dessus. Dans le menu déroulant "Sélectionner un projet Google Cloud", sélectionnez le projet Google Cloud spécifique dans la liste des projets que vous avez créés.

Projet de connexion

👉 Ouvrez le terminal dans l'IDE cloud, Nouveau terminal.

👉 Dans le terminal, vérifiez que vous êtes déjà authentifié et que le projet est défini sur votre ID de projet à l'aide de la commande suivante:

gcloud auth list

👉 Exécutez ensuite la commande suivante:

gcloud config set project <YOUR_PROJECT_ID>

👉 Exécutez la commande suivante pour activer les API Google Cloud nécessaires:

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

Cette opération peut prendre quelques minutes.

Activer Gemini Code Assist dans Cloud Shell IDE

Cliquez sur le bouton Aide au code dans le panneau de gauche, comme indiqué, puis sélectionnez une dernière fois le bon projet Google Cloud. Si vous êtes invité à activer l'API Cloud AI Companion, veuillez le faire et continuer. Une fois que vous avez sélectionné votre projet Google Cloud, vérifiez que le message d'état Cloud Code s'affiche dans la barre d'état et que Code Assist est également activé à droite, dans la barre d'état, comme illustré ci-dessous:

Activer l&#39;assistance au code

Configurer l'autorisation

👉Configurer l'autorisation du compte de service

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"

Accorder des autorisations 👉Cloud Storage (lecture/écriture):

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 (lecture/écriture):

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 (utilisateur):

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

👉 Secret Manager (lecture):

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

👉 Validez le résultat dans la console IAM.Console IAM

4. Créer le premier agent

Avant de nous plonger dans les systèmes multi-agents complexes, nous devons établir un élément de base fondamental: un agent fonctionnel unique. Dans cette section, nous allons commencer par créer un agent "fournisseur de livres" simple. L'agent du fournisseur de livres reçoit une catégorie en entrée et utilise un LLM Gemini pour générer une représentation JSON d'un livre dans cette catégorie. Il diffuse ensuite ces recommandations de livres en tant que point de terminaison d'API REST .

Fournisseur de services de réservation

👉 Dans un autre onglet de votre navigateur, ouvrez la Google Cloud Console. Dans le menu de navigation (☰), accédez à "Cloud Run". Cliquez sur le bouton "+ ... ÉCRIRE UNE FONCTION".

Create Function

👉 Ensuite, nous allons configurer les paramètres de base de la fonction Cloud Run:

  • Nom du service : book-provider
  • Région : us-central1
  • Environnement d'exécution: Python 3.12
  • Authentification: Allow unauthenticated invocations sur "Activée".

👉 Laissez les autres paramètres par défaut et cliquez sur Créer. Vous êtes alors redirigé vers l'éditeur de code source.

Des fichiers main.py et requirements.txt préremplis s'affichent.

main.py contient la logique métier de la fonction, requirements.txt contient les packages nécessaires.

👉Nous sommes maintenant prêts à écrire du code. Mais avant de nous lancer, voyons si Gemini Code Assist peut nous aider. Revenez à l'éditeur Cloud Shell, cliquez sur l'icône Gemini Code Assist, puis collez la requête suivante dans la zone de requête: 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 génère ensuite une solution potentielle, en fournissant à la fois le code source et un fichier de dépendances requirements.txt.

Nous vous encourageons à comparer le code généré par Code Assist à la solution correcte testée fournie ci-dessous. Vous pouvez ainsi évaluer l'efficacité de l'outil et identifier d'éventuelles divergences. Bien que les LLM ne doivent jamais être utilisés de manière aveugle, Code Assist peut être un excellent outil pour le prototypage rapide et la génération de structures de code initiales. Il doit être utilisé pour vous donner un bon départ.

Comme il s'agit d'un atelier, nous allons utiliser le code validé fourni ci-dessous. Toutefois, n'hésitez pas à tester le code généré par Code Assist à votre guise pour mieux comprendre ses fonctionnalités et ses limites.

👉 Revenez à l'éditeur de code source de la fonction Cloud Run (dans l'autre onglet du navigateur). Remplacez soigneusement le contenu existant de main.py par le code ci-dessous:

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-1.0-pro")

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)

👉 Remplacez le contenu de requirements.txt par le code suivant:

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

👉 Nous allons définir le point d'entrée de la fonction: recommended.

03-02-function-create.png

👉 Cliquez sur ENREGISTRER ET DÉPLOYER pour déployer la fonction. Attendez la fin du processus de déploiement. Cloud Console affiche l'état. Cette opération peut prendre plusieurs minutes.

texte alternatif 👉 Une fois le déploiement effectué, revenez dans l'éditeur Cloud Shell et exécutez la commande suivante dans le terminal:

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

Il doit afficher des données sur les livres au format 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"}
]

Félicitations ! Vous avez déployé une fonction Cloud Run avec succès. Il s'agit de l'un des services que nous allons intégrer lors du développement de notre agent Aidemy.

5. Outils de création: connecter des agents à un service et des données RESTFUL

Téléchargez le projet de référence Bootstrap. Assurez-vous d'être dans l'éditeur Cloud Shell. Dans le terminal, exécutez la commande suivante :

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

Une fois cette commande exécutée, un dossier nommé aidemy-bootstrap est créé dans votre environnement Cloud Shell.

Dans le volet "Explorer" de l'éditeur Cloud Shell (généralement sur la gauche), vous devriez maintenant voir le dossier créé lorsque vous avez cloné le dépôt Git aidemy-bootstrap. Ouvrez le dossier racine de votre projet dans l'explorateur. Vous y trouverez un sous-dossier planner. Ouvrez-le également. Explorateur de projets

Commençons à créer les outils que nos agents utiliseront pour devenir vraiment utiles. Comme vous le savez, les LLM sont excellents pour raisonner et générer du texte, mais ils ont besoin d'accéder à des ressources externes pour effectuer des tâches concrètes et fournir des informations précises et à jour. Considérez ces outils comme le "couteau suisse" de l'agent, qui lui permet d'interagir avec le monde.

Lorsque vous créez un agent, il est facile de coder en dur un grand nombre de détails. Cela crée un agent qui n'est pas flexible. Au lieu de cela, en créant et en utilisant des outils, l'agent a accès à des logiques ou systèmes externes, ce qui lui offre les avantages du LLM et de la programmation traditionnelle.

Dans cette section, nous allons créer les bases de l'agent de planification, que les enseignants utiliseront pour générer des plans de cours. Avant que l'agent ne commence à générer un plan, nous souhaitons définir des limites en fournissant plus de détails sur le sujet et le thème. Nous allons créer trois outils:

  1. Appel d'API REST:interaction avec une API préexistante pour récupérer des données.
  2. Requête de base de données:récupération de données structurées à partir d'une base de données Cloud SQL.
  3. Recherche Google:permet d'accéder aux informations en temps réel sur le Web.

Récupérer des recommandations de livres à partir d'une API

Commençons par créer un outil qui récupère des recommandations de livres à partir de l'API book-provider que nous avons déployée dans la section précédente. Cela montre comment un agent peut exploiter les services existants.

Recommander un livre

Dans l'éditeur Cloud Shell, ouvrez le projet aidemy-bootstrap que vous avez cloné dans la section précédente. 👉 Modifiez le fichier book.py dans le dossier planner et collez le code suivant:

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."))

Explication :

  • recommend_book(query: str): cette fonction utilise la requête de l'utilisateur en entrée.
  • Interaction avec le LLM: utilise le LLM pour extraire la catégorie de la requête. Cela montre comment vous pouvez utiliser le LLM pour créer des paramètres pour les outils.
  • Appel d'API: envoie une requête POST à l'API du fournisseur de livres, en transmettant la catégorie et le nombre de livres souhaités.

👉 Pour tester cette nouvelle fonction, définissez la variable d'environnement, puis exécutez la commande suivante :

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

👉 Installez les dépendances et exécutez le code pour vous assurer qu'il fonctionne:

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

Ignorez la fenêtre pop-up d'avertissement Git.

Une chaîne JSON contenant des recommandations de livres récupérées à partir de l'API du fournisseur de livres devrait s'afficher.

[{"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"}]

Si vous voyez cette information, cela signifie que le premier outil fonctionne correctement.

Au lieu de créer explicitement un appel d'API RESTful avec des paramètres spécifiques, nous utilisons le langage naturel ("Je suis en train de suivre un cours…"). L'agent extrait ensuite intelligemment les paramètres nécessaires (comme la catégorie) à l'aide du traitement du langage naturel, ce qui montre comment il exploite la compréhension du langage naturel pour interagir avec l'API.

comparer appel

👉 Supprimez le code de test suivant de 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."))

Obtenir des données sur le programme à partir d'une base de données

Nous allons ensuite créer un outil qui extrait des données de programme structurées à partir d'une base de données PostgreSQL Cloud SQL. Cela permet à l'agent d'accéder à une source d'informations fiable pour la planification des cours.

create db

👉 Exécutez les commandes suivantes dans le terminal pour créer une instance Cloud SQL nommée aidemy . Ce processus peut prendre un certain temps.

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

👉 Ensuite, créez une base de données nommée aidemy-db dans la nouvelle instance.

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

Vérifions l'instance dans Cloud SQL dans la console Google Cloud. Une instance Cloud SQL nommée aidemy devrait s'afficher. Cliquez sur le nom de l'instance pour en afficher les détails. Sur la page d'informations de l'instance Cloud SQL, cliquez sur "SQL Studio" dans le menu de navigation de gauche. Un nouvel onglet s'affiche.

Cliquez pour vous connecter à la base de données. Se connecter à SQL Studio

Sélectionnez aidemy-db comme base de données. Saisissez postgres comme utilisateur et 1234qwer comme mot de passe. connexion à sql studio

👉 Dans l'éditeur de requête SQL Studio, collez le code SQL suivant:

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.');

Ce code SQL crée une table nommée curriculums et insère des exemples de données. Cliquez sur Run (Exécuter) pour exécuter le code SQL. Un message de confirmation vous indiquant que les commandes ont bien été exécutées devrait s'afficher.

👉 Développez l'explorateur, recherchez la table que vous venez de créer, puis cliquez sur query (Requête). Un nouvel onglet de l'éditeur s'ouvre avec le code SQL généré pour vous.

table de sélection sql studio

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

👉 Cliquez sur Run (Exécuter).

La table des résultats doit afficher les lignes de données que vous avez insérées à l'étape précédente, ce qui confirme que la table et les données ont été créées correctement.

Maintenant que vous avez créé une base de données avec des exemples de données de programme remplis, nous allons créer un outil pour la récupérer.

👉 Dans l'éditeur Cloud Code, modifiez le fichier curriculums.py dans le dossier aidemy-bootstrap et collez le code suivant:

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"]

    encoded_db_user = os.environ.get("DB_USER")
    print(f"--------------------------->db_user: {db_user!r}")  
    print(f"--------------------------->db_pass: {db_pass!r}") 
    print(f"--------------------------->db_name: {db_name!r}") 

    ip_type = IPTypes.PRIVATE if os.environ.get("PRIVATE_IP") else IPTypes.PUBLIC

    connector = Connector()

    def getconn() -> pg8000.dbapi.Connection:
        conn: pg8000.dbapi.Connection = connector.connect(
            instance_connection_name,
            "pg8000",
            user=db_user,
            password=db_pass,
            db=db_name,
            ip_type=ip_type,
        )
        return conn

    pool = sqlalchemy.create_engine(
        "postgresql+pg8000://",
        creator=getconn,
        pool_size=2,
        max_overflow=2,
        pool_timeout=30,  # 30 seconds
        pool_recycle=1800,  # 30 minutes
    )
    return pool



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

    raise ValueError(
        "Missing database connection type. Please define one of INSTANCE_HOST, INSTANCE_UNIX_SOCKET, or INSTANCE_CONNECTION_NAME"
    )

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 = init_connection_pool()

Explication :

  • Variables d'environnement: le code récupère les identifiants de base de données et les informations de connexion à partir de variables d'environnement (plus d'informations ci-dessous).
  • connect_with_connector(): cette fonction utilise le connecteur Cloud SQL pour établir une connexion sécurisée à la base de données.
  • get_curriculum(year: int, subject: str): cette fonction prend l'année et le sujet en entrée, interroge la table des programmes et renvoie la description du programme correspondant.

👉Avant de pouvoir exécuter le code, nous devons définir certaines variables d'environnement. Dans le terminal, exécutez la commande suivante:

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"

👉 Pour tester, ajoutez le code suivant à la fin de curriculums.py:

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

👉 Exécutez le code:

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

La description du programme de mathématiques de la 6e classe devrait s'afficher sur la console.

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

Si la description du programme s'affiche, cela signifie que l'outil de base de données fonctionne correctement. Arrêtez le script en appuyant sur Ctrl+C.

👉 Supprimez le code de test suivant de curriculums.py.

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

👉 Quittez l'environnement virtuel. Dans le terminal, exécutez la commande suivante:

deactivate

6. Outils de création: accéder aux informations en temps réel depuis le Web

Enfin, nous allons créer un outil qui utilise l'intégration de Gemini 2 et de la recherche Google pour accéder aux informations en temps réel du Web. Cela permet à l'agent de rester informé et de fournir des résultats pertinents.

L'intégration de Gemini 2 à l'API Google Search améliore les capacités des agents en leur fournissant des résultats de recherche plus précis et plus pertinents en fonction du contexte. Cela permet aux agents d'accéder à des informations à jour et de baser leurs réponses sur des données réelles, ce qui réduit les hallucinations. L'intégration améliorée des API facilite également les requêtes en langage naturel, ce qui permet aux agents de formuler des requêtes de recherche complexes et nuancées.

Rechercher

Cette fonction prend en entrée une requête de recherche, un programme, une matière et une année, et utilise l'API Gemini et l'outil de recherche Google pour récupérer des informations pertinentes sur Internet. Si vous regardez attentivement, vous constaterez qu'il utilise le SDK de l'IA générative de Google pour effectuer des appels de fonction sans utiliser d'autre framework.

👉 Modifiez search.py dans le dossier aidemy-bootstrap et collez le code suivant:

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)

Explication :

  • Définir un outil : google_search_tool : encapsuler l'objet GoogleSearch dans un outil
  • search_latest_resource(search_text: str, subject: str, year: int): cette fonction prend en entrée une requête de recherche, un sujet et une année, et utilise l'API Gemini pour effectuer une recherche Google. Modèle Gemini
  • GenerateContentConfig: indiquez qu'il a accès à l'outil Google Search.

Le modèle Gemini analyse en interne le texte de recherche et détermine s'il peut répondre directement à la question ou s'il doit utiliser l'outil GoogleSearch. Il s'agit d'une étape critique qui se produit dans le processus de raisonnement du LLM. Le modèle a été entraîné pour reconnaître les situations dans lesquelles des outils externes sont nécessaires. Si le modèle décide d'utiliser l'outil Google Search, le SDK Google Generative AI gère l'appel réel. Le SDK prend la décision du modèle et les paramètres qu'il génère, puis les envoie à l'API Google Search. Cette partie est masquée pour l'utilisateur dans le code.

Le modèle Gemini intègre ensuite les résultats de recherche dans sa réponse. Il peut utiliser ces informations pour répondre à la question de l'utilisateur, générer un résumé ou effectuer une autre tâche.

👉 Pour tester, exécutez le code:

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

La réponse de l'API Gemini Search devrait contenir les résultats de recherche associés à "Syllabus for Year 5 Mathematics" (Programme de mathématiques de l'année 5). La sortie exacte dépend des résultats de recherche, mais il s'agit d'un objet JSON contenant des informations sur la recherche.

Si des résultats de recherche s'affichent, cela signifie que l'outil de recherche Google fonctionne correctement. Arrêtez le script en appuyant sur Ctrl+C.

👉 Et supprimez la dernière partie du code.

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)

👉 Quittez l'environnement virtuel. Dans le terminal, exécutez la commande suivante:

deactivate

Félicitations ! Vous avez maintenant créé trois outils puissants pour votre agent de planification: un connecteur d'API, un connecteur de base de données et un outil de recherche Google. Ces outils lui permettront d'accéder aux informations et aux fonctionnalités dont il a besoin pour créer des plans de cours efficaces.

7. Orchestration avec LangGraph

Maintenant que nous avons créé nos outils individuels, il est temps de les orchestrer à l'aide de LangGraph. Cela nous permettra de créer un agent de planification plus sophistiqué, capable de décider intelligemment quels outils utiliser et quand, en fonction de la demande de l'utilisateur.

LangGraph est une bibliothèque Python conçue pour faciliter la création d'applications multi-acteurs avec état à l'aide de grands modèles de langage (LLM). Il s'agit d'un framework permettant d'orchestrer des conversations et des workflows complexes impliquant des LLM, des outils et d'autres agents.

Concepts clés

  • Structure de graphique:LangGraph représente la logique de votre application sous la forme d'un graphique orienté. Chaque nœud du graphique représente une étape du processus (par exemple, un appel à un LLM, une invocation d'outil ou une vérification conditionnelle). Les arêtes définissent le flux d'exécution entre les nœuds.
  • État:LangGraph gère l'état de votre application à mesure qu'elle se déplace dans le graphique. Cet état peut inclure des variables telles que l'entrée de l'utilisateur, les résultats des appels d'outils, les sorties intermédiaires des LLM et toute autre information à conserver entre les étapes.
  • Nœuds:chaque nœud représente un calcul ou une interaction. Elles peuvent être:
    • Nœuds d'outil:utilisez un outil (par exemple, effectuez une recherche sur le Web ou interrogez une base de données).
    • Nœuds de fonction:exécutez une fonction Python.
  • Edges (Arêtes) : relient les nœuds, définissant le flux d'exécution. Elles peuvent être:
    • Arêtes directes:flux simple et inconditionnel d'un nœud à un autre.
    • Arcs conditionnels:le flux dépend du résultat d'un nœud conditionnel.

LangGraph

Nous allons utiliser LangGraph pour implémenter l'orchestration. Modifions le fichier aidemy.py dans le dossier aidemy-bootstrap pour définir notre logique LangGraph. 👉 Ajoutez le code suivant à la fin de 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"])} 

Cette fonction est chargée de récupérer l'état actuel de la conversation, de fournir un message système au LLM, puis de lui demander de générer une réponse. Le LLM peut répondre directement à l'utilisateur ou choisir d'utiliser l'un des outils disponibles.

tools (outils) : cette liste représente l'ensemble d'outils dont dispose l'agent. Il contient trois fonctions d'outil que nous avons définies dans les étapes précédentes: get_curriculum, search_latest_resource et recommend_book. llm.bind_tools(tools) : "lie" la liste des outils à l'objet llm. Lier les outils indique au LLM qu'ils sont disponibles et lui fournit des informations sur leur utilisation (par exemple, les noms des outils, les paramètres qu'ils acceptent et leur fonction).

Nous allons utiliser LangGraph pour implémenter l'orchestration. 👉 Ajoutez le code suivant à la fin de 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")

Explication :

  • StateGraph(MessagesState):crée un objet StateGraph. Un StateGraph est un concept fondamental de LangGraph. Il représente le workflow de votre agent sous la forme d'un graphique, où chaque nœud du graphique représente une étape du processus. Il s'agit de définir le plan selon lequel l'agent raisonnera et agira.
  • Arête conditionnelle:provenant du nœud "determine_tool", l'argument tools_condition est probablement une fonction qui détermine l'arête à suivre en fonction de la sortie de la fonction determine_tool. Les arêtes conditionnelles permettent au graphique de se ramifier en fonction de la décision du LLM concernant l'outil à utiliser (ou s'il doit répondre directement à l'utilisateur). C'est là que l'agent peut s'appuyer sur son "intelligence" : il peut adapter dynamiquement son comportement en fonction de la situation.
  • Boucle:ajoute un arc au graphique qui relie le nœud "tools" au nœud "determine_tool". Cela crée une boucle dans le graphique, ce qui permet à l'agent d'utiliser des outils à plusieurs reprises jusqu'à ce qu'il ait rassemblé suffisamment d'informations pour accomplir la tâche et fournir une réponse satisfaisante. Cette boucle est cruciale pour les tâches complexes qui nécessitent plusieurs étapes de raisonnement et de collecte d'informations.

Voyons maintenant comment notre agent planificateur orchestre les différents outils.

Ce code exécute la fonction prep_class avec une entrée utilisateur spécifique, simulant une demande de création d'un plan d'enseignement de géométrie en mathématiques de cinquième année, à l'aide du programme, des recommandations de livres et des dernières ressources Internet.

Si vous avez fermé votre terminal ou que les variables d'environnement ne sont plus définies, exécutez à nouveau les commandes suivantes.

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"

👉 Exécutez le code:

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

Consultez le journal dans le terminal. Vous devez voir des preuves que l'agent appelle les trois outils (obtenir le programme scolaire, obtenir des recommandations de livres et rechercher les dernières ressources) avant de fournir le plan d'enseignement final. Cela montre que l'orchestration de LangGraph fonctionne correctement et que l'agent utilise intelligemment tous les outils disponibles pour répondre à la demande de l'utilisateur.

================================ 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**
.........

Arrêtez le script en appuyant sur Ctrl+C.

👉Remplacez maintenant le code de test par une invite différente, qui nécessite l'appel d'outils différents.

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")

Si vous avez fermé votre terminal ou que les variables d'environnement ne sont plus définies, exécutez à nouveau les commandes suivantes.

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"

👉 Exécutez à nouveau le code:

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

Qu'avez-vous remarqué cette fois-ci ? Quels outils l'agent a-t-il appelés ? Vous devriez constater que l'agent n'appelle que l'outil search_latest_resource cette fois. En effet, l'invite ne spécifie pas qu'elle a besoin des deux autres outils, et notre LLM est suffisamment intelligent pour ne pas les appeler.

================================ 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.

Arrêtez le script en appuyant sur Ctrl+C. 👉Supprimez le code de test pour garder votre fichier aidemy.py propre:

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")

Maintenant que la logique de l'agent est définie, lançons l'application Web Flask. Les enseignants pourront ainsi interagir avec l'agent via une interface basée sur des formulaires familière. Bien que les interactions avec les chatbots soient courantes avec les LLM, nous avons opté pour une interface utilisateur d'envoi de formulaires classique, car elle peut être plus intuitive pour de nombreux enseignants.

Si vous avez fermé votre terminal ou que les variables d'environnement ne sont plus définies, exécutez à nouveau les commandes suivantes.

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"

👉Lancez maintenant l'UI Web.

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

Recherchez les messages de démarrage dans la sortie du terminal Cloud Shell. Flask imprime généralement des messages indiquant qu'il s'exécute et sur quel port.

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.

👉 Dans le menu "Aperçu sur le Web", sélectionnez "Prévisualiser sur le port 8080". Cloud Shell ouvre un nouvel onglet ou une nouvelle fenêtre de navigateur avec l'aperçu Web de votre application.

Page Web

Dans l'interface de l'application, sélectionnez 5 pour l'année, sélectionnez l'objet Mathematics et saisissez Geometry dans la demande de module complémentaire Limite de quota.

Plutôt que de rester les yeux rivés sur l'écran en attendant la réponse, passez au terminal de l'éditeur Cloud. Vous pouvez observer la progression et les messages de sortie ou d'erreur générés par votre fonction dans le terminal de l'émulateur. 😁

👉 Arrêtez le script en appuyant sur Ctrl+C dans le terminal.

👉 Quittez l'environnement virtuel:

deactivate

8. Déployer l'agent de planification dans le cloud

Créer et transférer l'image vers le registre

Présentation

👉 Il est temps de déployer cette application dans le cloud. Dans le terminal, créez un dépôt d'artefacts pour stocker l'image Docker que nous allons créer.

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

Le message "Dépôt créé [agent-repository]" doit s'afficher.

👉 Exécutez la commande suivante pour créer l'image Docker.

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

👉 Nous devons ajouter un nouveau tag à l'image pour qu'elle soit hébergée dans Artifact Registry au lieu de GCR, puis transférer l'image taguée vers Artifact Registry:

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

Une fois le transfert terminé, vous pouvez vérifier que l'image est bien stockée dans Artifact Registry. Accédez à Artifact Registry dans la console Google Cloud. L'image aidemy-planner doit s'afficher dans le dépôt agent-repository. Image du planificateur Aidemy

Sécuriser les identifiants de base de données avec Secret Manager

Pour gérer et accéder de manière sécurisée aux identifiants de base de données, nous allons utiliser Google Cloud Secret Manager. Cela évite d'encoder en dur des informations sensibles dans le code de l'application et renforce la sécurité.

👉 Nous allons créer des secrets individuels pour le nom d'utilisateur, le mot de passe et le nom de la base de données. Cette approche nous permet de gérer chaque identifiant de manière indépendante. Dans le terminal, exécutez la commande suivante:

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=-

L'utilisation de Secret Manager est une étape importante pour sécuriser votre application et éviter l'exposition accidentelle d'identifiants sensibles. Il respecte les bonnes pratiques de sécurité pour les déploiements cloud.

Déployer dans Cloud Run

Cloud Run est une plate-forme sans serveur entièrement gérée qui vous permet de déployer des applications conteneurisées rapidement et facilement. Elle élimine la gestion de l'infrastructure, ce qui vous permet de vous concentrer sur l'écriture et le déploiement de votre code. Nous allons déployer notre planificateur en tant que service Cloud Run.

👉 Dans la console Google Cloud, accédez à Cloud Run. Cliquez sur DÉPLOYER UN CONTENEUR, puis sélectionnez SERVICE. Configurez votre service Cloud Run:

Cloud Run

  1. Image du conteneur: cliquez sur "Sélectionner" dans le champ d'URL. Recherchez l'URL de l'image que vous avez transférée vers Artifact Registry (par exemple, us-central1-docker.pkg.dev/VOTRE_ID_PROJET/agent-repository/agent-planner/VOTRE_IMG).
  2. Service name (Nom du service) : aidemy-planner
  3. Région: sélectionnez la région us-central1.
  4. Authentification: pour les besoins de cet atelier, vous pouvez autoriser "Autoriser les appels non authentifiés". Pour la production, vous devrez probablement limiter l'accès.
  5. Onglet Container(s) (Développer "Containers, Network" (Conteneurs, réseau)):
    • Onglet "Paramètres" :
      • Ressource 
        • mémoire : 2 Go
    • Onglet "Variables et secrets" :
      • Variables d'environnement:
        • Ajoutez le nom: GOOGLE_CLOUD_PROJECT et la valeur: <YOUR_PROJECT_ID>.
        • Ajoutez le nom: BOOK_PROVIDER_URL et la valeur: <YOUR_BOOK_PROVIDER_FUNCTION_URL>
      • Secrets exposés en tant que variables d'environnement:
        • Ajoutez nom: DB_USER, secret: sélectionnez db-user et version:latest.
        • Ajoutez nom: DB_PASS, secret: sélectionnez db-pass et version:latest.
        • Ajoutez nom: DB_NAME, secret: sélectionnez db-name et version:latest.

Exécutez la commande suivante dans le terminal si vous devez récupérer votre YOUR_BOOK_PROVIDER_FUNCTION_URL:

gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)"

Définir un secret

Laissez l'option "Autre" par défaut.

👉 Cliquez sur CRÉER.

Cloud Run déploie votre service.

Une fois le service déployé, cliquez dessus pour accéder à sa page d'informations. L'URL déployée s'affiche en haut de la page.

URL

Dans l'interface de l'application, sélectionnez 7 pour l'année, choisissez Mathematics comme objet et saisissez Algebra dans le champ "Demande de module complémentaire". Cela fournira à l'agent le contexte nécessaire pour générer un plan de cours personnalisé.

Félicitations ! Vous avez créé un plan de cours à l'aide de notre puissant agent d'IA. Cela démontre le potentiel des agents à réduire considérablement la charge de travail et à simplifier les tâches, ce qui améliore l'efficacité et facilite la vie des enseignants.

9. Systèmes multi-agents

Maintenant que nous avons implémenté l'outil de création de plan de cours, nous allons nous concentrer sur la création du portail étudiant. Ce portail leur permettra d'accéder aux quiz, aux récapitulatifs audio et aux devoirs liés à leur cours. Compte tenu de la portée de cette fonctionnalité, nous allons exploiter la puissance des systèmes multi-agents pour créer une solution modulaire et évolutive.

Comme nous l'avons vu précédemment, au lieu de s'appuyer sur un seul agent pour tout gérer, un système multi-agent permet de décomposer la charge de travail en tâches plus petites et spécialisées, chacune gérée par un agent dédié. Cette approche présente plusieurs avantages clés:

Modularité et facilité de maintenance: au lieu de créer un seul agent qui fait tout, créez des agents plus petits et spécialisés, dont les responsabilités sont bien définies. Cette modularité facilite la compréhension, la maintenance et le débogage du système. Lorsqu'un problème survient, vous pouvez l'isoler à un agent spécifique, au lieu d'avoir à passer au crible un codebase massif.

Évolutivité: l'évolutivité d'un seul agent complexe peut constituer un goulot d'étranglement. Avec un système multi-agents, vous pouvez faire évoluer des agents individuels en fonction de leurs besoins spécifiques. Par exemple, si un agent traite un volume élevé de requêtes, vous pouvez facilement démarrer d'autres instances de cet agent sans affecter le reste du système.

Spécificité des équipes : imaginez que vous ne demandiez pas à un ingénieur de créer une application complète à partir de zéro. Vous réunissez plutôt une équipe de spécialistes, chacun étant expert dans un domaine particulier. De même, un système multi-agents vous permet de tirer parti des points forts de différents LLM et outils, en les attribuant aux agents les plus adaptés à des tâches spécifiques.

Développement parallèle: différentes équipes peuvent travailler sur différents agents en même temps, ce qui accélère le processus de développement. Étant donné que les agents sont indépendants, les modifications apportées à un agent sont moins susceptibles d'avoir un impact sur les autres.

Architecture basée sur les événements

Pour permettre une communication et une coordination efficaces entre ces agents, nous allons utiliser une architecture basée sur les événements. Cela signifie que les agents réagiront aux "événements" qui se produisent dans le système.

Les agents s'abonnent à des types d'événements spécifiques (par exemple, "Plan de cours généré", "Devoir créé"). Lorsqu'un événement se produit, les agents concernés sont avertis et peuvent réagir en conséquence. Ce découplage favorise la flexibilité, l'évolutivité et la réactivité en temps réel.

Présentation

Pour commencer, nous devons trouver un moyen de diffuser ces événements. Pour ce faire, nous allons configurer un sujet Pub/Sub. Commençons par créer un sujet intitulé plan.

👉 Accédez à Pub/Sub dans la console Google Cloud, puis cliquez sur le bouton "Créer un sujet".

👉Configurez le sujet avec l'ID/nom plan et décochez Add a default subscription. Laissez les autres paramètres par défaut, puis cliquez sur Create (Créer).

La page Pub/Sub s'actualise et le sujet que vous venez de créer doit maintenant figurer dans le tableau. Créer un sujet

Intégrons maintenant la fonctionnalité de publication d'événements Pub/Sub dans notre agent de planification. Nous allons ajouter un nouvel outil qui envoie un événement "plan" au sujet Pub/Sub que nous venons de créer. Cet événement indique aux autres agents du système (comme ceux du portail étudiant) qu'un nouveau plan d'enseignement est disponible.

👉 Revenez à l'éditeur de code Cloud et ouvrez le fichier app.py situé dans le dossier planner. Nous allons ajouter une fonction qui publie l'événement. Remplacez :

##ADD SEND PLAN EVENT FUNCTION HERE

avec

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: cette fonction prend le plan de cours généré en entrée, crée un client éditeur Pub/Sub, crée le chemin d'accès au sujet, convertit le plan de cours en chaîne JSON , puis publie le message sur le sujet.
  • Liste des outils: la fonction send_plan_event est ajoutée à la liste des outils, ce qui la rend disponible pour l'agent.

Dans le même dossier, dans le fichier app.py, 👉 modifiez l'invite pour demander à l'agent d'envoyer l'événement du plan de cours au sujet Pub/Sub après avoir généré le plan de cours. Remplacer

### ADD send_plan_event CALL

avec les informations suivantes:

send_plan_event(teaching_plan)

En ajoutant l'outil send_plan_event et en modifiant l'invite, nous avons permis à notre agent planificateur de publier des événements sur Pub/Sub, ce qui permet aux autres composants de notre système de réagir à la création de nouveaux plans de cours. Nous disposerons désormais d'un système multi-agent fonctionnel dans les sections suivantes.

10. Doter les élèves d'outils pour les aider à progresser grâce aux questionnaires à la demande

Imaginez un environnement d'apprentissage dans lequel les élèves ont accès à un nombre illimité de quiz adaptés à leurs plans d'apprentissage spécifiques. Ces quiz fournissent des commentaires immédiats, y compris des réponses et des explications, ce qui favorise une meilleure compréhension du contenu. C'est ce potentiel que nous souhaitons exploiter avec notre portail de quiz optimisé par l'IA.

Pour donner vie à cette vision, nous allons créer un composant de génération de quiz capable de créer des questions à choix multiples en fonction du contenu du plan d'enseignement.

Présentation

👉 Dans le volet "Explorer" de l'éditeur Cloud Code, accédez au dossier portal. Ouvrez le fichier quiz.py, puis copiez et collez le code suivant à la fin du fichier.

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-1.5-pro", 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


Dans l'agent, il crée un analyseur de sortie JSON spécialement conçu pour comprendre et structurer la sortie du LLM. Il utilise le modèle QuizQuestion que nous avons défini précédemment pour s'assurer que la sortie analysée est conforme au format approprié (question, options et réponse).

👉 Exécutez les commandes suivantes dans le terminal pour configurer un environnement virtuel, installer les dépendances et démarrer l'agent:

cd ~/aidemy-bootstrap/portal/
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py

Utilisez la fonctionnalité d'aperçu sur le Web de Cloud Shell pour accéder à l'application en cours d'exécution. Cliquez sur le lien "Quizzes" (Quizzes) dans la barre de navigation supérieure ou dans la fiche de la page d'index. Trois quiz générés de manière aléatoire devraient s'afficher pour l'élève. Ces quiz sont basés sur le plan d'enseignement et démontrent la puissance de notre système de génération de quiz basé sur l'IA.

Quiz

Pour arrêter le processus en cours d'exécution localement, appuyez sur Ctrl+C dans le terminal.

Pensée Gemini 2 pour les explications

D'accord, nous avons des quiz, c'est un bon début. Mais que se passe-t-il si les élèves se trompent ? C'est là que l'apprentissage se produit, n'est-ce pas ? Si nous pouvons expliquer pourquoi sa réponse était incorrecte et comment trouver la bonne, il est beaucoup plus susceptible de s'en souvenir. Cela permet également de dissiper toute confusion et de renforcer leur confiance.

C'est pourquoi nous allons sortir l'artillerie lourde: le modèle de "pensée" de Gemini 2. C'est comme si vous donniez à l'IA un peu plus de temps pour réfléchir avant de vous expliquer. Il peut ainsi fournir des commentaires plus détaillés et plus pertinents.

Nous voulons voir s'il peut aider les élèves en les assistant, en répondant à leurs questions et en leur expliquant les choses en détail. Pour le tester, commençons par un sujet réputé difficile : le calcul.

Présentation

👉 Tout d'abord, accédez à l'éditeur de code Cloud, dans answer.py, dans le dossier portal, remplacez

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

avec l'extrait de code suivant:

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)

Il s'agit d'une application de chaîne de langage très simple qui initialise le modèle Gemini 2 Flash, dans lequel nous lui demandons de jouer le rôle d'un enseignant utile et de fournir des explications.

👉 Exécutez la commande suivante dans le terminal:

cd ~/aidemy-bootstrap/portal/
python answer.py

Un résultat semblable à l'exemple fourni dans les instructions d'origine doit s'afficher. Le modèle actuel ne fournit peut-être pas d'explication aussi détaillée.

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!

Dans le fichier answer.py, remplacez gemini-2.0-flash-001 par gemini-2.0-flash-thinking-exp-01-21 dans la fonction answer_thinking pour le nom du modèle.

Cela modifie le LLM qui raisonne davantage, ce qui l'aidera à générer de meilleures explications. Et exécutez-le à nouveau.

👉 Exécutez le nouveau modèle de réflexion pour le tester:

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

Voici un exemple de réponse du modèle de pensée, qui est beaucoup plus détaillée et fournit une explication détaillée de la façon de résoudre le problème de calcul. Cela met en évidence la puissance des modèles de "pensée" pour générer des explications de haute qualité. Le résultat doit ressembler à ce qui suit :

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.

SUPPRIMEZ le code de test suivant de 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)

👉 Exécutez les commandes suivantes dans le terminal pour configurer un environnement virtuel, installer les dépendances et démarrer l'agent:

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

👉 Utilisez la fonctionnalité d'aperçu sur le Web de Cloud Shell pour accéder à l'application en cours d'exécution. Cliquez sur le lien "Quizzes" (Quizzes), répondez à tous les quiz, assurez-vous d'avoir au moins une mauvaise réponse, puis cliquez sur "Submit" (Envoyer).

réponses réfléchies

Plutôt que de rester les yeux rivés sur l'écran en attendant la réponse, passez au terminal de l'éditeur Cloud. Vous pouvez observer la progression et les messages de sortie ou d'erreur générés par votre fonction dans le terminal de l'émulateur. 😁

Pour arrêter le processus en cours d'exécution localement, appuyez sur Ctrl+C dans le terminal.

11. Orchestration des agents avec Eventarc

Jusqu'à présent, le portail étudiant génère des quiz en fonction d'un ensemble par défaut de plans d'enseignement. C'est utile, mais cela signifie que notre agent de planification et l'agent de quiz du portail ne communiquent pas vraiment entre eux. Vous vous souvenez que nous avons ajouté une fonctionnalité permettant à l'agent de planification de publier ses nouveaux plans de cours sur un sujet Pub/Sub ? Il est maintenant temps de l'associer à notre agent du portail.

Présentation

Nous voulons que le portail mette automatiquement à jour le contenu de ses quiz chaque fois qu'un nouveau plan d'enseignement est généré. Pour ce faire, nous allons créer un point de terminaison dans le portail pouvant recevoir ces nouveaux plans.

👉 Dans le volet "Explorer" de l'éditeur Cloud Code, accédez au dossier portal. Ouvrez le fichier app.py pour le modifier. Ajoutez le code suivant entre ## Ajoutez votre code ici:

## Add your code here

@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
## Add your code here

Recréer et déployer sur Cloud Run

OK. Vous devrez mettre à jour et redéployer nos agents de planificateur et de portail sur Cloud Run. Ils disposent ainsi du code le plus récent et sont configurés pour communiquer via des événements.

Présentation du déploiement

👉 Nous allons ensuite recompiler et transférer l'image de l'agent planner dans le terminal:

cd ~/aidemy-bootstrap/planner/
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

👉 Nous allons faire de même pour l'image de l'agent du portail:

cd ~/aidemy-bootstrap/portal/
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

Dans Artifact Registry, les images de conteneur aidemy-planner et aidemy-portal devraient s'afficher.

Dépôt de conteneurs

👉 Retournez dans le terminal et exécutez cette commande pour mettre à jour l'image Cloud Run de l'agent de planification:

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

Le résultat doit ressembler à ce qui suit :

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

Notez l'URL du service. Il s'agit du lien vers votre agent de planification déployé.

👉 Exécutez cette commande pour créer l'instance Cloud Run de l'agent du portail.

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}

Le résultat doit ressembler à ce qui suit :

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

Notez l'URL du service. Il s'agit du lien vers votre portail pour les élèves déployé.

Créer le déclencheur Eventarc

Mais voici la grande question: comment ce point de terminaison est-il informé lorsqu'un nouveau plan est en attente dans le sujet Pub/Sub ? C'est là qu'Eventarc intervient pour vous sauver la mise.

Eventarc sert de pont, écoutant des événements spécifiques (comme un nouveau message arrivant dans notre sujet Pub/Sub) et déclenchant automatiquement des actions en réponse. Dans notre cas, il détectera quand un nouveau plan de cours sera publié, puis enverra un signal au point de terminaison de notre portail pour l'informer qu'il est temps de le mettre à jour.

Eventarc gère la communication basée sur les événements. Nous pouvons ainsi connecter facilement notre agent de planification et notre agent de portail, ce qui crée un système d'apprentissage vraiment dynamique et réactif. C'est comme si vous aviez un messager intelligent qui envoie automatiquement les derniers plans de cours au bon endroit.

👉 Dans la console, accédez à Eventarc.

👉 Cliquez sur le bouton "+ CRÉER UN DÉclencheur".

Configurez le déclencheur (principes de base):

  • Nom du déclencheur: plan-topic-trigger
  • Types de déclencheurs: sources Google.
  • Fournisseur d'événements: Cloud Pub/Sub
  • Type d'événement: google.cloud.pubsub.topic.v1.messagePublished
  • Région: us-central1.
  • Sujet Cloud Pub/Sub : sélectionnez plan
  • GRANT le compte de service avec le rôle roles/iam.serviceAccountTokenCreator
  • Destination de l'événement: Cloud Run
  • Service Cloud Run: aidemy-portal
  • Chemin de l'URL du service: /new_teaching_plan
  • Ignorer le message (autorisation refusée sur "locations/me-central2" (ou elle n'existe peut-être pas).)

Cliquez sur "Créer".

La page "Déclencheurs Eventarc" s'actualise et le déclencheur que vous venez de créer doit maintenant apparaître dans le tableau.

👉Accédez maintenant au planificateur et demandez un nouveau plan de cours. Cette fois, essayez l'année 5, l'objet science et la requête Add-no atoms.

Exécutez cette commande dans le terminal si vous avez oublié l'emplacement de votre agent de planification.

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

Attendez ensuite une ou deux minutes. Ce délai a été introduit en raison des limites de facturation de cet atelier. En temps normal, il ne devrait pas y avoir de retard.

Enfin, accédez au portail des élèves. Vous devriez voir que les quiz ont été mis à jour et qu'ils correspondent désormais au nouveau plan d'enseignement que vous venez de générer. Cela démontre que l'intégration d'Eventarc dans le système Aidemy a bien été effectuée.

Exécutez cette commande dans le terminal si vous avez oublié l'emplacement de votre agent de portail.

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

Aidemy-celebrate

Félicitations ! Vous avez créé un système multi-agent sur Google Cloud, en exploitant une architecture basée sur des événements pour améliorer l'évolutivité et la flexibilité. Vous avez posé des bases solides, mais il y a encore bien plus à découvrir. Pour en savoir plus sur les avantages réels de cette architecture, découvrir la puissance de l'API Multimodal Live de Gemini 2 et apprendre à implémenter l'orchestration à un seul chemin avec LangGraph, n'hésitez pas à poursuivre les deux chapitres suivants.

12. OPTIONNEL: Recaps audio avec Gemini

Gemini peut comprendre et traiter des informations provenant de diverses sources, comme du texte, des images et même de l'audio, ce qui ouvre de nouvelles possibilités d'apprentissage et de création de contenus. La capacité de Gemini à "voir", "entendre" et "lire" ouvre de véritables possibilités pour offrir des expériences utilisateur créatives et attrayantes.

Au-delà de la création de visuels ou de texte, une autre étape importante de l'apprentissage est la synthèse et le récapitulatif efficaces. Réfléchissez: combien de fois vous souvenez-vous plus facilement des paroles d'une chanson accrocheuse que de ce que vous avez lu dans un manuel ? Le son peut être incroyablement mémorable. C'est pourquoi nous allons exploiter les fonctionnalités multimodaux de Gemini pour générer des récapitulatifs audio de nos plans de cours. Cela permettra aux élèves de revoir le contenu de manière pratique et attrayante, ce qui peut améliorer la rétention et la compréhension grâce à la puissance de l'apprentissage auditif.

Présentation de l&#39;API Live

Nous avons besoin d'un emplacement pour stocker les fichiers audio générés. Cloud Storage offre une solution évolutive et fiable.

👉 Accédez à Storage (Stockage) dans la console. Cliquez sur "Buckets" (Buckets) dans le menu de gauche. Cliquez sur le bouton "+ CRÉER" en haut de la page.

👉 Configurez votre bucket:

  • nom de bucket: aidemy-recap-<UNIQUE_NAME> IMPORTANT: Assurez-vous de définir un nom de bucket unique commençant par "aidemy-recap-". Ce nom unique est essentiel pour éviter les conflits de dénomination lors de la création de votre bucket Cloud Storage.
  • region: us-central1.
  • Classe de stockage: "Standard". Le stockage standard convient aux données fréquemment consultées.
  • Contrôle des accès: laissez l'option "Contrôle des accès uniforme" sélectionnée par défaut. Cela permet un contrôle des accès cohérent au niveau du bucket.
  • Options avancées: pour cet atelier, les paramètres par défaut sont généralement suffisants. Cliquez sur le bouton CRÉER pour créer votre bucket.

Un pop-up peut s'afficher concernant la protection contre l'accès public. Laissez la case cochée et cliquez sur Confirm.

Le bucket que vous venez de créer s'affiche maintenant dans la liste "Buckets" (Buckets). Notez le nom de votre bucket, car vous en aurez besoin plus tard.

👉 Dans le terminal de l'éditeur de code Cloud, exécutez les commandes suivantes pour accorder au compte de service l'accès au bucket:

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"

👉 Dans l'éditeur de code Cloud, ouvrez audio.py dans le dossier course. Collez le code suivant à la fin du fichier:

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-west1" #To workaround onRamp qouta 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-1.0-pro",
                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))
  • Connexion de streaming: une connexion persistante est d'abord établie avec le point de terminaison de l'API Live. Contrairement à un appel d'API standard, où vous envoyez une requête et obtenez une réponse, cette connexion reste ouverte pour un échange de données continu.
  • Configuration multimodale: utilisez la configuration pour spécifier le type de sortie souhaité (dans ce cas, audio). Vous pouvez même spécifier les paramètres que vous souhaitez utiliser (par exemple, la sélection de la voix, l'encodage audio).
  • Traitement asynchrone: cette API fonctionne de manière asynchrone, ce qui signifie qu'elle ne bloque pas le thread principal en attendant la fin de la génération audio. En traitant les données en temps réel et en envoyant la sortie par blocs, il offre une expérience quasi instantanée.

La question clé est la suivante: quand ce processus de génération audio doit-il s'exécuter ? Idéalement, les Recaps audio devraient être disponibles dès qu'un nouveau plan de cours est créé. Comme nous avons déjà implémenté une architecture basée sur des événements en publiant le plan de cours dans un sujet Pub/Sub, nous pouvons simplement nous y abonner.

Toutefois, nous ne générons pas très souvent de nouveaux plans de cours. Il serait peu efficace de laisser un agent en cours d'exécution en permanence et d'attendre de nouveaux plans. C'est pourquoi il est tout à fait logique de déployer cette logique de génération audio en tant que fonction Cloud Run.

En le déployant en tant que fonction, il reste inactif jusqu'à ce qu'un nouveau message soit publié dans le sujet Pub/Sub. Dans ce cas, la fonction est automatiquement déclenchée, ce qui génère les Recaps audio et les stocke dans notre bucket.

👉 Dans le dossier course du fichier main.py, ce fichier définit la fonction Cloud Run qui sera déclenchée lorsqu'un nouveau plan d'enseignement sera disponible. Il reçoit le plan et lance la génération du Recap audio. Ajoutez l'extrait de code suivant à la fin du fichier.

@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: ce décorateur marque la fonction comme une fonction Cloud Run qui sera déclenchée par des CloudEvents.

Tester localement

👉 Nous allons l'exécuter dans un environnement virtuel et installer les bibliothèques Python nécessaires à la fonction Cloud Run.

cd ~/aidemy-bootstrap/courses
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

👉 L'émulateur de fonctions Cloud Run nous permet de tester notre fonction localement avant de la déployer sur Google Cloud. Démarrez un émulateur local en exécutant la commande suivante:

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

👉 Lorsque l'émulateur est en cours d'exécution, vous pouvez lui envoyer des CloudEvents de test pour simuler la publication d'un nouveau plan de cours. Dans un nouveau terminal:

Deux terminaux

👉 Exécuter:

  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=="
    }
  }'

Plutôt que d'attendre la réponse, passez à l'autre terminal Cloud Shell. Vous pouvez observer la progression et les messages de sortie ou d'erreur générés par votre fonction dans le terminal de l'émulateur. 😁

Dans le deuxième terminal, vous devriez voir OK renvoyé.

👉 Vérifiez les données dans le bucket. Accédez à Cloud Storage, sélectionnez l'onglet "Bucket" (Bucket), puis aidemy-recap-xxx.

Bucket

👉 Dans le terminal exécutant l'émulateur, saisissez ctrl+c pour quitter. Et fermez le deuxième terminal. Fermez le deuxième terminal, puis exécutez deactivate pour quitter l'environnement virtuel.

deactivate

Déployer sur Google Cloud

Présentation du déploiement 👉 Après avoir effectué des tests en local, il est temps de déployer l'agent de cours sur Google Cloud. Dans le terminal, exécutez les commandes suivantes:

cd ~/aidemy-bootstrap/courses
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

Vérifiez le déploiement en accédant à Cloud Run dans la console Google Cloud.Un nouveau service nommé "courses-agent" devrait s'afficher.

Liste Cloud Run

Pour vérifier la configuration du déclencheur, cliquez sur le service courses-agent pour en afficher les détails. Accédez à l'onglet "DÉclencheurs".

Un déclencheur configuré pour écouter les messages publiés dans le sujet du plan devrait s'afficher.

Déclencheur Cloud Run

Enfin, voyons-le fonctionner de bout en bout.

👉 Ensuite, nous devons configurer l'agent du portail pour qu'il sache où trouver les fichiers audio générés. Dans le terminal, exécutez la commande suivante:

export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
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

👉Essayez de générer un nouveau plan de cours sur la page de l'agent de planification. Le démarrage peut prendre quelques minutes. Ne vous inquiétez pas, il s'agit d'un service sans serveur. Obtenez l'URL de votre agent de planification (si vous ne l'avez pas sous la main, exécutez cette commande dans le terminal):

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

Après avoir généré le nouveau plan, attendez deux à trois minutes pour que l'audio soit généré. Cela prendra encore quelques minutes en raison des limites de facturation de ce compte de laboratoire.

Vous pouvez vérifier si la fonction courses-agent a bien reçu le plan de cours en consultant l'onglet "DÉclencheurs" de la fonction. Actualisez régulièrement la page. Vous devriez voir que la fonction a été appelée. Si la fonction n'a pas été appelée au bout de deux minutes, vous pouvez réessayer de générer le plan de cours. Toutefois, évitez de générer des plans de manière répétée et rapide, car chaque plan généré sera consommé et traité de manière séquentielle par l'agent, ce qui peut créer un arriéré.

Déclencher l&#39;observation

👉 Accédez au portail, puis cliquez sur "Cours". Trois cartes s'affichent, chacune affichant un Recap audio. Pour trouver l'URL de votre agent de portail:

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

Cliquez sur "Lire" pour chaque cours afin de vous assurer que les récapitulatifs audio correspondent au plan d'enseignement que vous venez de générer. Cours du portail

Quittez l'environnement virtuel.

deactivate

13. (FACULTATIF) Collaboration basée sur les rôles avec Gemini et DeepSeek

Il est essentiel de recueillir différents points de vue, en particulier lorsque vous créez des devoirs attrayants et réfléchis. Nous allons maintenant créer un système multi-agents qui s'appuie sur deux modèles différents ayant des rôles distincts pour générer des devoirs: l'un favorise la collaboration, et l'autre l'auto-apprentissage. Nous utiliserons une architecture "à un seul coup", où le workflow suit un chemin fixe.

Générateur de devoirs Gemini

Présentation de Gemini Nous allons commencer par configurer la fonction Gemini pour générer des devoirs axés sur la collaboration. Modifiez le fichier gemini.py situé dans le dossier assignment.

👉 Collez le code suivant à la fin du fichier 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()

Il utilise le modèle Gemini pour générer des devoirs.

Nous sommes prêts à tester l'agent Gemini.

👉 Exécutez les commandes suivantes dans le terminal pour configurer l'environnement:

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

👉 Vous pouvez exécuter le code pour le tester:

python gemini.py

Vous devriez voir un devoir qui comporte plus de travail de groupe dans la sortie. Le test d'assertion à la fin affichera également les résultats.

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. 
....

Arrêtez avec ctl+c et nettoyez le code de test. SUPPRIMEZ le code suivant de 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()

Configurer le générateur de devoirs DeepSeek

Bien que les plates-formes d'IA cloud soient pratiques, les LLM auto-hébergés peuvent être essentiels pour protéger la confidentialité des données et assurer leur souveraineté. Nous allons déployer le plus petit modèle DeepSeek (1,5 milliard de paramètres) sur une instance Cloud Compute Engine. Il existe d'autres façons de l'héberger, comme sur la plate-forme Vertex AI de Google ou sur votre instance GKE. Toutefois, comme il ne s'agit que d'un atelier sur les agents d'IA et que je ne veux pas vous retenir ici indéfiniment, utilisons la méthode la plus simple. Toutefois, si vous souhaitez explorer d'autres options, consultez le fichier deepseek-vertexai.py dans le dossier de devoir, qui fournit un exemple de code pour interagir avec les modèles déployés sur Vertex AI.

Présentation de Deepseek

👉 Exécutez cette commande dans le terminal pour créer une plate-forme LLM autohébergée Ollama:

cd ~/aidemy-bootstrap/assignment
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

Pour vérifier que l'instance Compute Engine est en cours d'exécution:

Accédez à Compute Engine > "Instances de VM" dans la console Google Cloud. ollama-instance devrait s'afficher avec une coche verte, indiquant qu'il est en cours d'exécution. Si ce n'est pas le cas, vérifiez que la zone est us-central1. Si ce n'est pas le cas, vous devrez peut-être le rechercher.

Liste Compute Engine

👉Nous allons installer le plus petit modèle DeepSeek et le tester. Dans l'éditeur Cloud Shell, dans un terminal Nouveau, exécutez la commande suivante pour accéder à l'instance GCE via SSH.

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

Une fois la connexion SSH établie, vous pouvez être invité à effectuer les opérations suivantes:

"Do you want to continue (Y/n)?"

Il vous suffit de saisir Y(la casse n'a pas d'importance) et d'appuyer sur Entrée pour continuer.

Vous serez peut-être ensuite invité à créer une phrase secrète pour la clé SSH. Si vous préférez ne pas utiliser de phrase secrète, appuyez simplement deux fois sur Entrée pour accepter la valeur par défaut (aucune phrase secrète).

👉Vous êtes maintenant dans la machine virtuelle. Extraction du plus petit modèle DeepSeek R1 et test de son fonctionnement.

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

👉 Quittez l'instance GCE et saisissez ce qui suit dans le terminal SSH:

exit

Fermez le nouveau terminal, puis revenez au terminal d'origine.

👉 N'oubliez pas de configurer la stratégie réseau pour que les autres services puissent accéder au LLM. Veuillez limiter l'accès à l'instance si vous souhaitez le faire en production. Pour ce faire, implémentez une connexion sécurisée pour le service ou limitez l'accès par adresse IP. Exécutez la commande suivante :

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

👉 Pour vérifier que votre stratégie de pare-feu fonctionne correctement, essayez d'exécuter la commande suivante:

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
        }'

Ensuite, nous allons travailler sur la fonction Deepseek dans l'agent d'attribution pour générer des devoirs mettant l'accent sur le travail individuel.

👉 Modifiez deepseek.py dans le dossier assignment et ajoutez l'extrait suivant à la fin.

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()

👉 Testons-le en exécutant la commande suivante:

cd ~/aidemy-bootstrap/assignment
source env/bin/activate
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

Vous devriez voir un devoir qui comporte plus de travail en auto-apprentissage.

**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.
....

👉 Arrêtez le ctl+c et nettoyez le code de test. SUPPRIMEZ le code suivant de 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()

Nous allons maintenant utiliser le même modèle Gemini pour combiner les deux devoirs en un seul. Modifiez le fichier gemini.py situé dans le dossier assignment.

👉 Collez le code suivant à la fin du fichier 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

Pour combiner les points forts des deux modèles, nous allons orchestrer un workflow défini à l'aide de LangGraph. Ce workflow se compose de trois étapes: d'abord, le modèle Gemini génère un devoir axé sur la collaboration ; ensuite, le modèle DeepSeek génère un devoir mettant l'accent sur le travail individuel ; enfin, Gemini synthétise ces deux devoirs en un seul devoir complet. Comme nous prédéfinissons la séquence d'étapes sans prise de décision par le LLM, il s'agit d'une orchestration à un seul chemin définie par l'utilisateur.

Présentation de la combinaison Langraph

👉 Collez le code suivant à la fin du fichier main.py dans le dossier 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 TestCreatAssignment(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()

👉 Pour tester initialement la fonction create_assignment et vérifier que le workflow combinant Gemini et DeepSeek est fonctionnel, exécutez la commande suivante:

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

Vous devriez voir quelque chose qui combine les deux modèles avec leur perspective individuelle pour les études et les travaux de groupe des élèves.

**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

👉 Arrêtez le ctl+c et nettoyez le code de test. SUPPRIMEZ le code suivant de main.py

import unittest

class TestCreatAssignment(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

Pour automatiser le processus de génération des devoirs et le rendre réactif aux nouveaux plans de cours, nous allons exploiter l'architecture événementielle existante. Le code suivant définit une fonction Cloud Run (generate_assignment) qui se déclenche chaque fois qu'un nouveau plan d'enseignement est publié dans le sujet Pub/Sub plan.

👉 Ajoutez le code suivant à la fin de main.py:

@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

Tester localement

Avant de la déployer sur Google Cloud, il est recommandé de tester la fonction Cloud Run localement. Cela permet d'accélérer l'itération et de faciliter le débogage.

Commencez par créer un bucket Cloud Storage pour stocker les fichiers de devoir générés, puis accordez au compte de service l'accès à ce bucket. Exécutez les commandes suivantes dans le terminal:

👉IMPORTANT: Assurez-vous de définir un nom ASSIGNMENT_BUCKET unique commençant par "aidemy-assignment-". Ce nom unique est essentiel pour éviter les conflits de dénomination lors de la création de votre bucket Cloud Storage. (Remplacez <YOUR_NAME> par un mot aléatoire)

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

👉 Exécutez ensuite la commande suivante:

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"

👉Démarrez maintenant l'émulateur de fonctions Cloud Run:

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

👉 Lorsque l'émulateur s'exécute dans un terminal, ouvrez un deuxième terminal dans Cloud Shell. Dans ce deuxième terminal, envoyez un CloudEvent de test à l'émulateur pour simuler la publication d'un nouveau plan d'enseignement:

Deux terminaux

  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=="
    }
  }'

Plutôt que d'attendre la réponse, passez à l'autre terminal Cloud Shell. Vous pouvez observer la progression et les messages de sortie ou d'erreur générés par votre fonction dans le terminal de l'émulateur. 😁

Le message "OK" doit s'afficher.

Pour vérifier que le devoir a bien été généré et stocké, accédez à la console Google Cloud, puis à Storage > "Cloud Storage". Sélectionnez le bucket aidemy-assignment que vous avez créé. Un fichier texte nommé assignment-{random number}.txt devrait s'afficher dans le bucket. Cliquez sur le fichier pour le télécharger et vérifier son contenu. Cela permet de vérifier qu'un nouveau fichier contient la nouvelle affectation générée.

12-01-assignment-bucket

👉 Dans le terminal exécutant l'émulateur, saisissez ctrl+c pour quitter. Et fermez le deuxième terminal. 👉 Dans le terminal exécutant l'émulateur, quittez également l'environnement virtuel.

deactivate

Présentation du déploiement

👉 Ensuite, nous allons déployer l'agent d'attribution dans le cloud.

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
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 

Pour vérifier le déploiement, accédez à la console Google Cloud, puis à Cloud Run.Un nouveau service nommé "cours-agent" devrait s'afficher. 12-03-function-list

Le workflow de génération de devoirs étant maintenant implémenté, testé et déployé, nous pouvons passer à l'étape suivante: rendre ces devoirs accessibles dans le portail des élèves.

14. FACULTATIF: Collaboration basée sur les rôles avec Gemini et DeepSeek (suite)

Génération de sites Web dynamiques

Pour améliorer le portail des élèves et le rendre plus attrayant, nous allons implémenter la génération HTML dynamique pour les pages de devoirs. L'objectif est de mettre automatiquement à jour le portail avec une conception fraîche et attrayante chaque fois qu'une nouvelle tâche est générée. Cela exploite les capacités de codage du LLM pour créer une expérience utilisateur plus dynamique et intéressante.

14-01-generate-html

👉 Dans l'éditeur Cloud Shell, modifiez le fichier render.py dans le dossier portal, puis remplacez

def render_assignment_page():
    return ""

avec l'extrait de code suivant:

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)}"

Il utilise le modèle Gemini pour générer dynamiquement du code HTML pour le devoir. Il utilise le contenu du devoir comme entrée et une invite pour demander à Gemini de créer une page HTML attrayante avec un style créatif.

Ensuite, nous allons créer un point de terminaison qui sera déclenché chaque fois qu'un nouveau document sera ajouté au bucket de devoirs:

👉 Dans le dossier du portail, modifiez le fichier app.py et ajoutez le code suivant dans ## Add your code here" comments, APRÈS la fonction new_teaching_plan:

## Add your code here

@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


## Add your code here

Lorsqu'elle est déclenchée, elle récupère le nom du fichier et le nom du bucket à partir des données de la requête, télécharge le contenu du devoir depuis Cloud Storage et appelle la fonction render_assignment_page pour générer le code HTML.

👉 Nous allons l'exécuter en local:

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

👉 Dans le menu "Aperçu sur le Web" en haut de la fenêtre Cloud Shell, sélectionnez "Prévisualiser sur le port 8080". Votre application s'ouvre dans un nouvel onglet du navigateur. Accédez au lien Devoir dans la barre de navigation. À ce stade, une page vide s'affiche. C'est normal, car nous n'avons pas encore établi le pont de communication entre l'agent d'attribution et le portail pour renseigner dynamiquement le contenu.

14-02-deployment-overview

👉 Pour intégrer ces modifications et déployer le code mis à jour, recompilez et transférez l'image de l'agent du portail:

cd ~/aidemy-bootstrap/portal/
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

👉 Après avoir transféré la nouvelle image, redéployez le service Cloud Run. Exécutez le script suivant pour appliquer la mise à jour de Cloud Run:

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

👉Nous allons maintenant déployer un déclencheur Eventarc qui écoute tout nouvel objet créé (finalisé) dans le bucket d'affectation. Ce déclencheur appelle automatiquement le point de terminaison /render_assignment sur le service du portail lorsqu'un nouveau fichier de devoir est créé.

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"

Pour vérifier que le déclencheur a bien été créé, accédez à la page Déclencheurs Eventarc dans la console Google Cloud. portal-assignment-trigger doit s'afficher dans le tableau. Cliquez sur le nom du déclencheur pour afficher ses détails. Déclencheur d&#39;attribution

L'activation du nouveau déclencheur peut prendre jusqu'à deux à trois minutes.

Pour voir la génération d'affectations dynamiques en action, exécutez la commande suivante pour trouver l'URL de votre agent de planification (si vous ne l'avez pas sous la main):

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

Recherchez l'URL de votre agent de portail:

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

Dans l'agent de planification, générez un nouveau plan de cours.

13-02-assignment

Après quelques minutes (pour permettre la génération de l'audio, de l'exercice et du rendu HTML), accédez au portail des élèves.

👉 Cliquez sur le lien "Devoir" dans la barre de navigation. Un devoir que vous venez de créer avec un code HTML généré dynamiquement devrait s'afficher. Chaque fois qu'un plan d'enseignement est généré, il doit s'agir d'un devoir dynamique.

13-02-assignment

Félicitations, vous avez terminé le système multi-agents Aidemy ! Vous avez acquis une expérience pratique et des insights précieux sur les points suivants:

  • Les avantages des systèmes multi-agents, y compris la modularité, l'évolutivité, la spécialisation et la maintenance simplifiée.
  • L'importance des architectures basées sur des événements pour créer des applications réactives et faiblement couplées.
  • L'utilisation stratégique des LLM, en associant le bon modèle à la tâche et en les intégrant à des outils pour avoir un impact réel.
  • Pratiques de développement cloud native à l'aide des services Google Cloud pour créer des solutions évolutives et fiables
  • L'importance de prendre en compte la confidentialité des données et les modèles d'auto-hébergement comme alternative aux solutions de fournisseurs.

Vous disposez désormais d'une base solide pour créer des applications sophistiquées optimisées par l'IA sur Google Cloud.

15. Défis et prochaines étapes

Félicitations pour avoir créé le système multi-agents Aidemy. Vous avez posé une base solide pour l'enseignement optimisé par l'IA. Examinons maintenant certains défis et améliorations potentielles pour étendre ses fonctionnalités et répondre à des besoins réels:

Apprentissage interactif avec des questions/réponses en direct :

  • Défi: Pouvez-vous exploiter l'API Live de Gemini 2 pour créer une fonctionnalité de questions/réponses en temps réel pour les élèves ? Imaginez une salle de classe virtuelle où les élèves peuvent poser des questions et recevoir des réponses immédiates basées sur l'IA.

Envoi et notation automatiques des devoirs :

  • Défi: Concevez et implémentez un système permettant aux élèves de rendre leurs devoirs numériquement et de les faire noter automatiquement par l'IA, avec un mécanisme permettant de détecter et d'éviter le plagiat. Ce défi est une excellente opportunité d'explorer la génération augmentée par récupération (RAG) afin d'améliorer la précision et la fiabilité des processus d'évaluation et de détection du plagiat.

aidemy-climb

16. Effectuer un nettoyage

Maintenant que nous avons créé et exploré notre système multi-agents Aidemy, il est temps de nettoyer notre environnement Google Cloud.

  1. Supprimer des services 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
  1. Supprimer un déclencheur 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
  1. Supprimer le sujet Pub/Sub
gcloud pubsub topics delete plan --project="$PROJECT_ID" --quiet
  1. Supprimer une instance Cloud SQL
gcloud sql instances delete aidemy --quiet
  1. Supprimer le dépôt Artifact Registry
gcloud artifacts repositories delete agent-repository --location=us-central1 --quiet
  1. Supprimer des secrets Secret Manager
gcloud secrets delete db-user --quiet
gcloud secrets delete db-pass --quiet
gcloud secrets delete db-name --quiet
  1. Supprimer l'instance Compute Engine (si elle a été créée pour Deepseek)
gcloud compute instances delete ollama-instance --zone=us-central1-a --quiet
  1. Supprimer la règle de pare-feu pour l'instance Deepseek
gcloud compute firewall-rules delete allow-ollama-11434 --quiet
  1. Supprimer les buckets 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 rb gs://$COURSE_BUCKET_NAME
gsutil rb gs://$ASSIGNMENT_BUCKET

aidemy-broom