1. Introduzione
Ciao! Quindi, ti piace l'idea degli agenti, piccoli aiutanti che possono fare le cose per te senza che tu debba muovere un dito, giusto? Fantastico! Ma diciamoci la verità: un solo agente non è sempre sufficiente, soprattutto quando si affrontano progetti più grandi e complessi. Probabilmente avrai bisogno di un intero team. È qui che entrano in gioco i sistemi multi-agente.
Gli agenti, quando sono basati su LLM, offrono una flessibilità incredibile rispetto alla codifica rigida tradizionale. Ma, e c'è sempre un ma, presentano una serie di sfide difficili. Ed è esattamente quello che approfondiremo in questo workshop.

Ecco cosa puoi aspettarti di imparare, per diventare un agente di livello superiore:
Creazione del primo agente con LangGraph: ci metteremo all'opera per creare il tuo agente utilizzando LangGraph, un framework molto diffuso. Imparerai a creare strumenti che si connettono a database, a sfruttare l'ultima API Gemini 2 per alcune ricerche su internet e a ottimizzare i prompt e le risposte, in modo che il tuo agente possa interagire non solo con i LLM, ma anche con i servizi esistenti. Ti mostreremo anche come funziona la chiamata di funzioni.
Orchestrazione degli agenti, a modo tuo: esploreremo diversi modi per orchestrare gli agenti, dai percorsi semplici e diretti a scenari più complessi con più percorsi. Consideralo come la direzione del flusso del tuo team di agenti.
Sistemi multi-agente: scoprirai come configurare un sistema in cui gli agenti possono collaborare e portare a termine le attività insieme, il tutto grazie a un'architettura basata sugli eventi.
Libertà degli LLM: utilizza il migliore per il lavoro: non siamo legati a un solo LLM. Vedrai come utilizzare più LLM, assegnando loro ruoli diversi per aumentare la capacità di risoluzione dei problemi utilizzando fantastici "modelli di pensiero".
Contenuti dinamici? Nessun problema. Immagina il tuo agente che crea contenuti dinamici personalizzati in tempo reale per ogni utente. Ti mostreremo come fare.
Portare tutto sul cloud con Google Cloud: non limitarti a giocare con un notebook. Ti mostreremo come progettare e implementare il tuo sistema multi-agente su Google Cloud in modo che sia pronto per il mondo reale.
Questo progetto sarà un buon esempio di come utilizzare tutte le tecniche di cui abbiamo parlato.
2. Architettura
Essere insegnante o lavorare nel settore dell'istruzione può essere molto gratificante, ma ammettiamolo, il carico di lavoro, soprattutto tutto il lavoro di preparazione, può essere impegnativo. Inoltre, spesso non c'è personale sufficiente e le ripetizioni possono essere costose. Per questo motivo, proponiamo un assistente all'insegnamento basato sull'AI. Questo strumento può alleggerire il carico di lavoro degli insegnanti e contribuire a colmare il divario causato dalla carenza di personale e dalla mancanza di tutoraggio a prezzi accessibili.
Il nostro assistente didattico AI può creare piani delle lezioni dettagliati, quiz divertenti, riepiloghi audio facili da seguire e compiti personalizzati. In questo modo, gli insegnanti possono concentrarsi su ciò che sanno fare meglio: entrare in contatto con gli studenti e aiutarli ad appassionarsi all'apprendimento.
Il sistema ha due siti: uno per gli insegnanti per creare piani di lezioni per le settimane successive,

e uno per gli studenti per accedere a quiz, riepiloghi audio e compiti. 
Bene, esaminiamo l'architettura che alimenta il nostro assistente per l'insegnamento, Aidemy. Come puoi vedere, l'abbiamo suddiviso in diversi componenti chiave, che lavorano tutti insieme per rendere possibile questa funzionalità.

Elementi e tecnologie architettonici chiave:
Google Cloud Platform (GCP): elemento centrale dell'intero sistema:
- Vertex AI: accede ai LLM Gemini di Google.
- Cloud Run: piattaforma serverless per il deployment di agenti e funzioni containerizzati.
- Cloud SQL: database PostgreSQL per i dati del curriculum.
- Pub/Sub ed Eventarc: la base dell'architettura basata sugli eventi, che consente la comunicazione asincrona tra i componenti.
- Cloud Storage: archivia i riepiloghi audio e i file dei compiti.
- Secret Manager: gestisce in modo sicuro le credenziali del database.
- Artifact Registry: archivia le immagini Docker per gli agenti.
- Compute Engine: per eseguire il deployment di LLM autogestiti anziché affidarsi a soluzioni di fornitori
LLM: il "cervello" del sistema:
- Modelli Gemini di Google: (Gemini x Pro, Gemini x Flash, Gemini x Flash Thinking) Utilizzati per la pianificazione delle lezioni, la generazione di contenuti, la creazione di HTML dinamico, la spiegazione dei quiz e la combinazione dei compiti.
- DeepSeek: utilizzato per l'attività specializzata di generare compiti di studio autonomo
LangChain e LangGraph: framework per lo sviluppo di applicazioni LLM
- Facilita la creazione di workflow multi-agente complessi.
- Consente l'orchestrazione intelligente di strumenti (chiamate API, query di database, ricerche web).
- Implementa l'architettura basata su eventi per la scalabilità e la flessibilità del sistema.
In sostanza, la nostra architettura combina la potenza degli LLM con dati strutturati e comunicazione basata su eventi, il tutto eseguito su Google Cloud. In questo modo, possiamo creare un assistente per l'insegnamento scalabile, affidabile ed efficace.
3. Prima di iniziare
Nella console Google Cloud, nella pagina di selezione del progetto, seleziona o crea un progetto Google Cloud. Verifica che la fatturazione sia attivata per il tuo progetto Cloud. Scopri come verificare se la fatturazione è abilitata per un progetto.
Abilitare Gemini Code Assist nell'IDE di Cloud Shell
👉 Nella console Google Cloud, vai a Strumenti Gemini Code Assist, attiva Gemini Code Assist senza costi accettando i termini e le condizioni.

Ignora la configurazione delle autorizzazioni e abbandona questa pagina.
Utilizzare l'editor di Cloud Shell
👉 Fai clic su Attiva Cloud Shell nella parte superiore della console Google Cloud (l'icona a forma di terminale nella parte superiore del riquadro di Cloud Shell), quindi fai clic sul pulsante "Apri editor" (ha l'aspetto di una cartella aperta con una matita). Si aprirà l'editor di codice di Cloud Shell nella finestra. Vedrai un Esplora file sul lato sinistro.

👉 Fai clic sul pulsante Accedi a Cloud Code nella barra di stato in basso, come mostrato. Autorizza il plug-in come indicato. Se nella barra di stato vedi Cloud Code - no project, selezionalo, poi seleziona "Select a Google Cloud Project" (Seleziona un progetto Google Cloud) nel menu a discesa e infine seleziona il progetto Google Cloud specifico dall'elenco dei progetti che hai creato.

👉 Apri il terminale nell'IDE cloud,
o
.
👉 Nel terminale, verifica di essere già autenticato e che il progetto sia impostato sul tuo ID progetto utilizzando il seguente comando:
gcloud auth list
👉 Esegui l'operazione assicurandoti di sostituire <YOUR_PROJECT_ID> con il tuo ID progetto:
echo <YOUR_PROJECT_ID> > ~/project_id.txt
gcloud config set project $(cat ~/project_id.txt)
👉 Esegui il comando seguente per abilitare le API Google Cloud necessarie:
gcloud services enable compute.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
aiplatform.googleapis.com \
eventarc.googleapis.com \
sqladmin.googleapis.com \
secretmanager.googleapis.com \
cloudbuild.googleapis.com \
cloudresourcemanager.googleapis.com \
cloudfunctions.googleapis.com \
cloudaicompanion.googleapis.com
L'operazione potrebbe richiedere alcuni minuti.
Configurazione dell'autorizzazione
👉 Configura l'autorizzazione per l'account di servizio. Nel terminale, esegui :
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
echo "Here's your SERVICE_ACCOUNT_NAME $SERVICE_ACCOUNT_NAME"
👉 Concedi le autorizzazioni. Nel terminale, esegui :
#Cloud Storage (Read/Write):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/storage.objectAdmin"
#Pub/Sub (Publish/Receive):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/pubsub.publisher"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/pubsub.subscriber"
#Cloud SQL (Read/Write):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/cloudsql.editor"
#Eventarc (Receive Events):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/iam.serviceAccountTokenCreator"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/eventarc.eventReceiver"
#Vertex AI (User):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/aiplatform.user"
#Secret Manager (Read):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/secretmanager.secretAccessor"
👉 Convalida il risultato nella console IAM
👉 Esegui questi comandi nel terminale per creare un'istanza Cloud SQL denominata aidemy. Ne avremo bisogno in seguito, ma poiché questa procedura può richiedere del tempo, la eseguiamo ora.
gcloud sql instances create aidemy \
--database-version=POSTGRES_14 \
--cpu=2 \
--memory=4GB \
--region=us-central1 \
--root-password=1234qwer \
--storage-size=10GB \
--storage-auto-increase
4. Creazione del primo agente
Prima di addentrarci in sistemi multi-agente complessi, dobbiamo stabilire un blocco di base fondamentale: un singolo agente funzionale. In questa sezione, faremo i primi passi creando un semplice agente "fornitore di libri". L'agente fornitore di libri prende una categoria come input e utilizza un LLM Gemini per generare un libro in formato JSON all'interno di quella categoria. Quindi, fornisce questi consigli sui libri come endpoint API REST .

👉 In un'altra scheda del browser, apri la console Google Cloud nel browser web. Nel menu di navigazione (☰), vai a "Cloud Run". Fai clic sul pulsante "+ ... SCRIVI UNA FUNZIONE".

👉 Successivamente, configureremo le impostazioni di base della funzione Cloud Run:
- Nome servizio:
book-provider - Regione:
us-central1 - Runtime:
Python 3.12 - Autenticazione:
Allow unauthenticated invocationssu Attivata.
👉 Lascia invariate le altre impostazioni predefinite e fai clic su Crea. Verrà visualizzato l'editor del codice sorgente.
Verranno visualizzati i file main.py e requirements.txt precompilati.
main.py conterrà la logica di business della funzione, mentre requirements.txt conterrà i pacchetti necessari.
👉 Ora siamo pronti per scrivere un po' di codice. Ma prima di iniziare, vediamo se Gemini Code Assist può darci una mano. Torna all'editor di Cloud Shell, fai clic sull'icona di Gemini Code Assist in alto per aprire la chat di Gemini Code Assist.

👉 Incolla la seguente richiesta nella casella del prompt:
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 genererà quindi una potenziale soluzione, fornendo sia il codice sorgente sia un file di dipendenza requirements.txt. (NON UTILIZZARE QUESTO CODICE)
Ti invitiamo a confrontare il codice generato da Code Assist con la soluzione corretta e testata fornita di seguito. In questo modo puoi valutare l'efficacia dello strumento e identificare eventuali discrepanze. Sebbene i modelli LLM non debbano mai essere considerati attendibili ciecamente, Code Assist può essere un ottimo strumento per la prototipazione rapida e la generazione di strutture di codice iniziali e deve essere utilizzato per un buon inizio.
Poiché si tratta di un workshop, procederemo con il codice verificato fornito di seguito. Tuttavia, non esitare a sperimentare con il codice generato da Code Assist quando hai tempo per comprendere meglio le sue funzionalità e limitazioni.
👉 Torna all'editor del codice sorgente di Cloud Run Functions (nell'altra scheda del browser). Sostituisci con attenzione i contenuti esistenti di main.py con il codice fornito di seguito:
import functions_framework
import json
from flask import Flask, jsonify, request
from langchain_google_vertexai import ChatVertexAI
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from pydantic import BaseModel, Field
import os
class Book(BaseModel):
bookname: str = Field(description="Name of the book")
author: str = Field(description="Name of the author")
publisher: str = Field(description="Name of the publisher")
publishing_date: str = Field(description="Date of publishing")
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
llm = ChatVertexAI(model_name="gemini-2.0-flash-lite-001")
def get_recommended_books(category):
"""
A simple book recommendation function.
Args:
category (str): category
Returns:
str: A JSON string representing the recommended books.
"""
parser = JsonOutputParser(pydantic_object=Book)
question = f"Generate a random made up book on {category} with bookname, author and publisher and publishing_date"
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
chain = prompt | llm | parser
response = chain.invoke({"query": question})
return json.dumps(response)
@functions_framework.http
def recommended(request):
request_json = request.get_json(silent=True) # Get JSON data
if request_json and 'category' in request_json and 'number_of_book' in request_json:
category = request_json['category']
number_of_book = int(request_json['number_of_book'])
elif request.args and 'category' in request.args and 'number_of_book' in request.args:
category = request.args.get('category')
number_of_book = int(request.args.get('number_of_book'))
else:
return jsonify({'error': 'Missing category or number_of_book parameters'}), 400
recommendations_list = []
for i in range(number_of_book):
book_dict = json.loads(get_recommended_books(category))
print(f"book_dict=======>{book_dict}")
recommendations_list.append(book_dict)
return jsonify(recommendations_list)
👉 Sostituisci i contenuti di requirements.txt con quanto segue:
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
👉 Imposteremo l'entry point della funzione: recommended

👉 Fai clic su SALVA E ESEGUI IL DEPLOYMENT (o SALVA E RIESEGUI IL DEPLOYMENT) per eseguire il deployment della funzione. Attendi il completamento del processo di deployment. La console Cloud mostrerà lo stato. L'operazione potrebbe richiedere alcuni minuti.
👉 Una volta eseguito il deployment, torna all'editor di Cloud Shell ed esegui questo comando nel terminale:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
curl -X POST -H "Content-Type: application/json" -d '{"category": "Science Fiction", "number_of_book": 2}' $BOOK_PROVIDER_URL
Dovrebbero essere visualizzati alcuni dati del libro in formato 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"}
]
Complimenti! Hai eseguito correttamente il deployment di una funzione Cloud Run. Questo è uno dei servizi che integreremo durante lo sviluppo del nostro agente Aidemy.
5. Strumenti di creazione: collegamento degli agenti al servizio RESTFUL e ai dati
Scarichiamo il progetto Bootstrap Skeleton, assicurandoci di essere nell'editor di Cloud Shell. Nel terminale, esegui
git clone https://github.com/weimeilin79/aidemy-bootstrap.git
Dopo aver eseguito questo comando, verrà creata una nuova cartella denominata aidemy-bootstrap nel tuo ambiente Cloud Shell.
Nel riquadro Explorer dell'editor di Cloud Shell (di solito sul lato sinistro), ora dovresti vedere la cartella creata quando hai clonato il repository Git aidemy-bootstrap. Apri la cartella principale del progetto in Explorer. Al suo interno troverai una sottocartella planner, aprila. 
Iniziamo a creare gli strumenti che i nostri agenti utilizzeranno per diventare davvero utili. Come sapete, gli LLM sono eccellenti nel ragionamento e nella generazione di testo, ma hanno bisogno di accedere a risorse esterne per svolgere attività nel mondo reale e fornire informazioni accurate e aggiornate. Pensa a questi strumenti come al "coltellino svizzero " dell'agente, che gli consente di interagire con il mondo.
Quando crei un agente, è facile codificare in modo rigido una miriade di dettagli. In questo modo viene creato un agente non flessibile. Invece, creando e utilizzando strumenti, l'agente ha accesso a logiche o sistemi esterni, il che gli offre i vantaggi sia del LLM sia della programmazione tradizionale.
In questa sezione, creeremo le basi per l'agente di pianificazione, che gli insegnanti utilizzeranno per generare programmi delle lezioni. Prima che l'agente inizi a generare un piano, vogliamo impostare dei limiti fornendo maggiori dettagli sulla materia e sull'argomento. Creeremo tre strumenti:
- Chiamata API RESTful: interazione con un'API preesistente per recuperare i dati.
- Query sul database:recupero di dati strutturati da un database Cloud SQL.
- Ricerca Google:accesso a informazioni in tempo reale dal web.
Recupero di consigli sui libri da un'API
Innanzitutto, creiamo uno strumento che recuperi i consigli sui libri dall'API book-provider che abbiamo implementato nella sezione precedente. Questo mostra come un agente può sfruttare i servizi esistenti.

Nell'editor di Cloud Shell, apri il progetto aidemy-bootstrap che hai clonato nella sezione precedente.
👉 Modifica il file book.py nella cartella planner, e incolla il seguente codice alla fine del file:
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."))
Spiegazione:
- recommend_book(query: str): questa funzione accetta come input la query di un utente.
- Interazione LLM: utilizza l'LLM per estrarre la categoria dalla query. Questo esempio mostra come utilizzare il modello LLM per creare parametri per gli strumenti.
- Chiamata API: invia una richiesta POST all'API book-provider, passando la categoria e il numero di libri desiderato.
👉 Per testare questa nuova funzione, imposta la variabile di ambiente ed esegui :
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
cd ~/aidemy-bootstrap/planner/
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
👉 Installa le dipendenze ed esegui il codice per assicurarti che funzioni:
cd ~/aidemy-bootstrap/planner/
python -m venv env
source env/bin/activate
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
pip install -r requirements.txt
python book.py
Dovresti visualizzare una stringa JSON contenente i consigli sui libri recuperati dall'API del fornitore di libri. I risultati vengono generati in modo casuale. I libri potrebbero non essere gli stessi, ma dovresti ricevere due consigli in formato 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"}]
Se vedi questo messaggio, significa che il primo strumento funziona correttamente.
Anziché creare esplicitamente una chiamata API RESTful con parametri specifici, utilizziamo il linguaggio naturale ("Sto seguendo un corso..."). L'agente estrae in modo intelligente i parametri necessari (come la categoria) utilizzando l'NLP, evidenziando come l'agente sfrutta la comprensione del linguaggio naturale per interagire con l'API.

👉Rimuovi il seguente codice di test da 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."))
Recuperare i dati del curriculum da un database
Successivamente, creeremo uno strumento che recupera i dati strutturati del curriculum da un database Cloud SQL PostgreSQL. In questo modo, l'agente può accedere a una fonte affidabile di informazioni per la pianificazione delle lezioni.

Ricordi l'istanza Cloud SQL aidemy che hai creato nel passaggio precedente? Ecco dove verrà utilizzato.
👉 Nel terminale, esegui il comando seguente per creare un database denominato aidemy-db nella nuova istanza.
gcloud sql databases create aidemy-db \
--instance=aidemy
Verifichiamo l'istanza in Cloud SQL nella console Google Cloud. Dovresti visualizzare un'istanza Cloud SQL denominata aidemy.
👉 Fai clic sul nome dell'istanza per visualizzarne i dettagli. 👉 Nella pagina dei dettagli dell'istanza Cloud SQL, fai clic su Cloud SQL Studio nel menu di navigazione a sinistra. Si aprirà una nuova scheda.
Seleziona aidemy-db come database, inserisci postgres come utente e 1234qwer come password.
Fai clic su Autentica.

👉 Nell'editor di query di SQL Studio, vai alla scheda Editor 1 e incolla il seguente codice SQL:
CREATE TABLE curriculums (
id SERIAL PRIMARY KEY,
year INT,
subject VARCHAR(255),
description TEXT
);
-- Inserting detailed curriculum data for different school years and subjects
INSERT INTO curriculums (year, subject, description) VALUES
-- Year 5
(5, 'Mathematics', 'Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.'),
(5, 'English', 'Developing reading comprehension, creative writing, and basic grammar, with a focus on storytelling and poetry.'),
(5, 'Science', 'Exploring basic physics, chemistry, and biology concepts, including forces, materials, and ecosystems.'),
(5, 'Computer Science', 'Basic coding concepts using block-based programming and an introduction to digital literacy.'),
-- Year 6
(6, 'Mathematics', 'Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.'),
(6, 'English', 'Introduction to persuasive writing, character analysis, and deeper comprehension of literary texts.'),
(6, 'Science', 'Forces and motion, the human body, and introductory chemical reactions with hands-on experiments.'),
(6, 'Computer Science', 'Introduction to algorithms, logical reasoning, and basic text-based programming (Python, Scratch).'),
-- Year 7
(7, 'Mathematics', 'Algebraic expressions, geometry, and introduction to statistics and probability.'),
(7, 'English', 'Analytical reading of classic and modern literature, essay writing, and advanced grammar skills.'),
(7, 'Science', 'Introduction to cells and organisms, chemical reactions, and energy transfer in physics.'),
(7, 'Computer Science', 'Building on programming skills with Python, introduction to web development, and cyber safety.');
Questo codice SQL crea una tabella denominata curriculums e inserisce alcuni dati di esempio.
👉 Fai clic su Esegui per eseguire il codice SQL. Dovresti visualizzare un messaggio di conferma che indica che le istruzioni sono state eseguite correttamente.
👉 Espandi l'explorer, trova la tabella appena creata curriculums e fai clic su query. Si aprirà una nuova scheda dell'editor con l'SQL generato per te.

SELECT * FROM
"public"."curriculums" LIMIT 1000;
👉 Fai clic su Esegui.
La tabella dei risultati dovrebbe mostrare le righe di dati inserite nel passaggio precedente, confermando che la tabella e i dati sono stati creati correttamente.
Ora che hai creato correttamente un database con dati del curriculum di esempio compilati, creeremo uno strumento per recuperarlo.
👉 Nell'editor di Cloud Code, modifica il file curriculums.py nella cartella aidemy-bootstrap e incolla il seguente codice alla fine del file:
def connect_with_connector() -> sqlalchemy.engine.base.Engine:
db_user = os.environ["DB_USER"]
db_pass = os.environ["DB_PASS"]
db_name = os.environ["DB_NAME"]
print(f"--------------------------->db_user: {db_user!r}")
print(f"--------------------------->db_pass: {db_pass!r}")
print(f"--------------------------->db_name: {db_name!r}")
connector = Connector()
pool = sqlalchemy.create_engine(
"postgresql+pg8000://",
creator=lambda: connector.connect(
instance_connection_name,
"pg8000",
user=db_user,
password=db_pass,
db=db_name,
),
pool_size=2,
max_overflow=2,
pool_timeout=30, # 30 seconds
pool_recycle=1800, # 30 minutes
)
return pool
def get_curriculum(year: int, subject: str):
"""
Get school curriculum
Args:
subject: User's request subject string
year: User's request year int
"""
try:
stmt = sqlalchemy.text(
"SELECT description FROM curriculums WHERE year = :year AND subject = :subject"
)
with db.connect() as conn:
result = conn.execute(stmt, parameters={"year": year, "subject": subject})
row = result.fetchone()
if row:
return row[0]
else:
return None
except Exception as e:
print(e)
return None
db = connect_with_connector()
Spiegazione:
- Variabili di ambiente: il codice recupera le credenziali del database e le informazioni di connessione dalle variabili di ambiente (maggiori dettagli di seguito).
- connect_with_connector(): questa funzione utilizza il connettore Cloud SQL per stabilire una connessione sicura al database.
- get_curriculum(year: int, subject: str): questa funzione accetta l'anno e la materia come input, esegue una query sulla tabella dei programmi scolastici e restituisce la descrizione del programma scolastico corrispondente.
👉 Prima di poter eseguire il codice, dobbiamo impostare alcune variabili di ambiente. Nel terminale, esegui:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉 Per testare, aggiungi il seguente codice alla fine di curriculums.py:
if __name__ == "__main__":
print(get_curriculum(6, "Mathematics"))
👉 Esegui il codice:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python curriculums.py
Dovresti vedere la descrizione del programma di studi per la matematica di 6ª elementare stampata nella console.
Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.
Se vedi la descrizione del curriculum, lo strumento di database funziona correttamente. Interrompi lo script premendo Ctrl+C se è ancora in esecuzione.
👉Rimuovi il seguente codice di test da curriculums.py
if __name__ == "__main__":
print(get_curriculum(6, "Mathematics"))
👉 Esci dall'ambiente virtuale eseguendo questo comando nel terminale:
deactivate
6. Strumenti di costruzione: accedi a informazioni in tempo reale dal web
Infine, creeremo uno strumento che utilizza l'integrazione di Gemini 2 e della Ricerca Google per accedere a informazioni in tempo reale dal web. In questo modo, l'agente rimane aggiornato e fornisce risultati pertinenti.
L'integrazione di Gemini 2 con l'API Google Search migliora le funzionalità dell'agente fornendo risultati di ricerca più accurati e pertinenti al contesto. In questo modo, gli agenti possono accedere a informazioni aggiornate e basare le loro risposte su dati reali, riducendo al minimo le allucinazioni. L'integrazione API migliorata facilita anche le query in linguaggio naturale, consentendo agli agenti di formulare richieste di ricerca complesse e sfumate.

Questa funzione prende come input una query di ricerca, un curriculum, una materia e un anno e utilizza l'API Gemini e lo strumento Ricerca Google per recuperare informazioni pertinenti da internet. Se guardi da vicino, utilizza l'SDK Google Generative AI per chiamare le funzioni senza utilizzare altri framework.
👉 Modifica search.py nella cartella aidemy-bootstrap e incolla il seguente codice alla fine del file:
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)
Spiegazione:
- Strumento di definizione - google_search_tool: wrapping dell'oggetto GoogleSearch all'interno di uno strumento
- search_latest_resource(search_text: str, subject: str, year: int): questa funzione accetta come input una query di ricerca, una materia e un anno e utilizza l'API Gemini per eseguire una ricerca su Google.
- GenerateContentConfig: definisci che ha accesso allo strumento GoogleSearch
Il modello Gemini analizza internamente search_text e determina se può rispondere direttamente alla domanda o se deve utilizzare lo strumento GoogleSearch. Si tratta di un passaggio fondamentale che avviene all'interno del processo di ragionamento del LLM. Il modello è stato addestrato a riconoscere le situazioni in cui sono necessari strumenti esterni. Se il modello decide di utilizzare lo strumento GoogleSearch, l'SDK Google Generative AI gestisce l'invocazione effettiva. L'SDK prende la decisione del modello e i parametri che genera e li invia all'API Google Search. Questa parte è nascosta all'utente nel codice.
Il modello Gemini integra quindi i risultati di ricerca nella sua risposta. Può utilizzare le informazioni per rispondere alla domanda dell'utente, generare un riepilogo o svolgere un'altra attività.
👉 Per testare, esegui il codice:
cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
source env/bin/activate
python search.py
Dovresti visualizzare la risposta dell'API Gemini Search contenente i risultati di ricerca relativi a "Programma di studi per matematica del 5° anno". L'output esatto dipende dai risultati di ricerca, ma sarà un oggetto JSON con informazioni sulla ricerca.
Se vedi i risultati di ricerca, lo strumento Ricerca Google funziona correttamente. Se lo script è ancora in esecuzione, interrompilo premendo Ctrl+C.
👉 Rimuovi l'ultima parte del codice.
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)
👉 Esci dall'ambiente virtuale eseguendo questo comando nel terminale:
deactivate
Complimenti! Ora hai creato tre potenti strumenti per l'agente di pianificazione: un connettore API, un connettore di database e uno strumento di ricerca Google. Questi strumenti consentiranno all'agente di accedere alle informazioni e alle funzionalità necessarie per creare piani di insegnamento efficaci.
7. Orchestrazione con LangGraph
Ora che abbiamo creato i nostri singoli strumenti, è il momento di orchestrarli utilizzando LangGraph. In questo modo potremo creare un agente "planner" più sofisticato in grado di decidere in modo intelligente quali strumenti utilizzare e quando, in base alla richiesta dell'utente.
LangGraph è una libreria Python progettata per semplificare la creazione di applicazioni stateful e multi-attore utilizzando modelli linguistici di grandi dimensioni (LLM). Consideralo un framework per orchestrare conversazioni e workflow complessi che coinvolgono LLM, strumenti e altri agenti.
Concetti principali:
- Struttura del grafico:LangGraph rappresenta la logica della tua applicazione come un grafico orientato. Ogni nodo nel grafico rappresenta un passaggio della procedura (ad es. una chiamata a un LLM, una chiamata di uno strumento, un controllo condizionale). Gli archi definiscono il flusso di esecuzione tra i nodi.
- Stato:LangGraph gestisce lo stato dell'applicazione mentre si sposta nel grafico. Questo stato può includere variabili come l'input dell'utente, i risultati delle chiamate di strumenti, gli output intermedi dei LLM e qualsiasi altra informazione che deve essere conservata tra i passaggi.
- Nodi:ogni nodo rappresenta un calcolo o un'interazione. Possono essere:
- Nodi degli strumenti:utilizza uno strumento (ad es. esegui una ricerca web, esegui una query su un database)
- Nodi funzione:esegui una funzione Python.
- Archi:collegano i nodi, definendo il flusso di esecuzione. Possono essere:
- Archi diretti:un flusso semplice e incondizionato da un nodo all'altro.
- Bordi condizionali:il flusso dipende dal risultato di un nodo condizionale.

Utilizzeremo LangGraph per implementare l'orchestrazione. Modifichiamo il file aidemy.py nella cartella aidemy-bootstrap per definire la logica di LangGraph.
👉 Aggiungi il seguente codice alla fine di
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"])}
Questa funzione è responsabile dell'acquisizione dello stato attuale della conversazione, della fornitura di un messaggio di sistema all'LLM e della richiesta all'LLM di generare una risposta. L'LLM può rispondere direttamente all'utente o scegliere di utilizzare uno degli strumenti disponibili.
Strumenti : questo elenco rappresenta l'insieme di strumenti a disposizione dell'agente. Contiene tre funzioni dello strumento che abbiamo definito nei passaggi precedenti: get_curriculum, search_latest_resource e recommend_book. llm.bind_tools(tools): "lega" l'elenco degli strumenti all'oggetto llm. Il binding degli strumenti indica all'LLM che questi strumenti sono disponibili e fornisce all'LLM informazioni su come utilizzarli (ad esempio, i nomi degli strumenti, i parametri che accettano e cosa fanno).
Utilizzeremo LangGraph per implementare l'orchestrazione.
👉 Aggiungi il seguente codice alla fine di
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")
Spiegazione:
StateGraph(MessagesState): crea un oggettoStateGraph. UnStateGraphè un concetto fondamentale in LangGraph. Rappresenta il flusso di lavoro dell'agente come un grafico, in cui ogni nodo rappresenta un passaggio del processo. Pensa a come definire il progetto di base per il modo in cui l'agente ragionerà e agirà.- Conditional Edge:a partire dal nodo
"determine_tool", l'argomentotools_conditionè probabilmente una funzione che determina quale bordo seguire in base all'output della funzionedetermine_tool. I bordi condizionali consentono al grafico di ramificarsi in base alla decisione del LLM su quale strumento utilizzare (o se rispondere direttamente all'utente). È qui che entra in gioco l'"intelligenza" dell'agente, che può adattare dinamicamente il proprio comportamento in base alla situazione. - Ciclo:aggiunge un bordo al grafico che collega il nodo
"tools"al nodo"determine_tool". In questo modo si crea un ciclo nel grafico, consentendo all'agente di utilizzare ripetutamente gli strumenti finché non ha raccolto informazioni sufficienti per completare l'attività e fornire una risposta soddisfacente. Questo ciclo è fondamentale per le attività complesse che richiedono più passaggi di ragionamento e raccolta di informazioni.
Ora testiamo l'agente di pianificazione per vedere come coordina i diversi strumenti.
Questo codice eseguirà la funzione prep_class con un input utente specifico, simulando una richiesta di creazione di un piano di insegnamento per la matematica di quinta elementare in geometria, utilizzando il curriculum, i consigli sui libri e le risorse internet più recenti.
👉 Nel terminale, se lo hai chiuso o le variabili di ambiente non sono più impostate, esegui di nuovo i seguenti comandi
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉 Esegui il codice:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
pip install -r requirements.txt
python aidemy.py
Controlla il log nel terminale. Prima di fornire il piano di insegnamento finale, dovresti vedere la prova che l'agente sta chiamando tutti e tre gli strumenti (ottenere il programma scolastico, ottenere consigli sui libri e cercare le risorse più recenti). Ciò dimostra che l'orchestrazione di LangGraph funziona correttamente e che l'agente utilizza in modo intelligente tutti gli strumenti disponibili per soddisfare la richiesta dell'utente.
================================ 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**
.........
Per interrompere lo script, premi Ctrl+C se è ancora in esecuzione.
👉 (QUESTO PASSAGGIO È FACOLTATIVO) sostituisci il codice di test con un prompt diverso, che richiede la chiamata di strumenti diversi.
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")
👉 Se hai chiuso il terminale o le variabili di ambiente non sono più impostate, esegui di nuovo i seguenti comandi
gcloud config set project $(cat ~/project_id.txt)
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉 (QUESTO PASSAGGIO È FACOLTATIVO, eseguilo SOLO SE hai eseguito il passaggio precedente) Esegui di nuovo il codice:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python aidemy.py
Cosa hai notato questa volta? Quali strumenti ha chiamato l'agente? Dovresti notare che questa volta l'agente chiama solo lo strumento search_latest_resource. Questo perché il prompt non specifica che ha bisogno degli altri due strumenti e il nostro LLM è abbastanza intelligente da non chiamarli.
================================ 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.
Per interrompere lo script, premi Ctrl+C.
👉 (NON SALTARE QUESTO PASSAGGIO!) Rimuovi il codice di test per mantenere pulito il file aidemy.py :
if __name__ == "__main__":
prep_class("I'm doing a course for year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan")
Ora che la logica dell'agente è definita, avviamo l'applicazione web Flask. In questo modo, gli insegnanti avranno a disposizione un'interfaccia basata su moduli familiare per interagire con l'agente. Sebbene le interazioni con i chatbot siano comuni con i modelli LLM, abbiamo optato per un'interfaccia utente di invio di moduli tradizionale, in quanto potrebbe essere più intuitiva per molti insegnanti.
👉 Se hai chiuso il terminale o le variabili di ambiente non sono più impostate, esegui di nuovo i seguenti comandi
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉 Ora avvia l'interfaccia utente web.
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python app.py
Cerca i messaggi di avvio nell'output del terminale Cloud Shell. Flask di solito stampa messaggi che indicano che è in esecuzione e su quale porta.
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.
👉 Dal menu "Anteprima web" nell'angolo in alto a destra, scegli Anteprima sulla porta 8080. Cloud Shell aprirà una nuova scheda o finestra del browser con l'anteprima web della tua applicazione.

Nell'interfaccia dell'applicazione, seleziona 5 per Anno, seleziona l'oggetto Mathematics e digita Geometry nella richiesta di componente aggiuntivo.
👉 Se hai abbandonato la UI dell'applicazione, torna indietro e dovresti vedere l'output generato.
👉 Nel terminale, interrompi lo script premendo Ctrl+C.
👉 Nel terminale, esci dall'ambiente virtuale:
deactivate
8. Deployment dell'agente di pianificazione nel cloud
Crea ed esegui il push dell'immagine nel registro

È ora di eseguire il deployment nel cloud.
👉 Nel terminale, crea un repository di artefatti per archiviare l'immagine Docker che creeremo.
gcloud artifacts repositories create agent-repository \
--repository-format=docker \
--location=us-central1 \
--description="My agent repository"
Dovresti visualizzare il messaggio Repository creato [agent-repository].
👉 Esegui questo comando per creare l'immagine Docker.
cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .
👉 Dobbiamo taggare nuovamente l'immagine in modo che sia ospitata in Artifact Registry anziché in GCR ed eseguire il push dell'immagine taggata in Artifact Registry:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
Una volta completato il push, puoi verificare che l'immagine sia stata archiviata correttamente in Artifact Registry.
👉 Vai ad Artifact Registry nella console Google Cloud. Dovresti trovare l'immagine aidemy-planner nel repository agent-repository. 
Protezione delle credenziali del database con Secret Manager
Per gestire e accedere in modo sicuro alle credenziali del database, utilizzeremo Google Cloud Secret Manager. In questo modo si evita l'hardcoding di informazioni sensibili nel codice dell'applicazione e si migliora la sicurezza.
Creeremo singoli secret per il nome utente, la password e il nome del database. Questo approccio ci consente di gestire ogni credenziale in modo indipendente.
👉 Nel terminale, esegui questo comando:
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'utilizzo di Secret Manager è un passaggio importante per proteggere l'applicazione e impedire l'esposizione accidentale di credenziali sensibili. Segue le best practice di sicurezza per i deployment cloud.
Esegui il deployment in Cloud Run
Cloud Run è una piattaforma serverless completamente gestita che consente di eseguire il deployment di applicazioni containerizzate in modo rapido e semplice. Astrae la gestione dell'infrastruttura, consentendoti di concentrarti sulla scrittura e sul deployment del codice. Eseguiremo il deployment del nostro pianificatore come servizio Cloud Run.
👉 Nella console Google Cloud, vai a "Cloud Run". Fai clic su ESEGUI IL DEPLOYMENT DEL CONTAINER e seleziona SERVICE. Configura il servizio Cloud Run:

- Immagine container: fai clic su "Seleziona" nel campo URL. Trova l'URL dell'immagine di cui hai eseguito il push in Artifact Registry (ad es. us-central1-docker.pkg.dev/YOUR_PROJECT_ID/agent-repository/aidemy-planner/YOUR_IMG).
- Nome del servizio:
aidemy-planner - Regione: seleziona la regione
us-central1. - Autenticazione: ai fini di questo workshop, puoi consentire "Consenti chiamate non autenticate". Per la produzione, probabilmente vorrai limitare l'accesso.
- Espandi la sezione Container, volumi, networking, sicurezza e imposta quanto segue nella scheda Container:
- Scheda Impostazioni:
- Risorse
- memoria : 2GiB
- Risorse
- Scheda Variabili e secret:
- Variabili di ambiente, aggiungi le seguenti variabili facendo clic sul pulsante + Aggiungi variabile:
- Aggiungi nome:
GOOGLE_CLOUD_PROJECTe valore: <YOUR_PROJECT_ID> - Aggiungi il nome:
BOOK_PROVIDER_URLe imposta il valore sull'URL della funzione del fornitore di libri, che puoi determinare utilizzando il seguente comando nel terminale:gcloud config set project $(cat ~/project_id.txt) gcloud run services describe book-provider \ --region=us-central1 \ --project=$PROJECT_ID \ --format="value(status.url)"
- Aggiungi nome:
- Nella sezione Secret esposti come variabili di ambiente, aggiungi i seguenti secret facendo clic sul pulsante + Fai riferimento come secret:
- Aggiungi nome:
DB_USER, secret: selezionadb-usere versione:latest - Aggiungi nome:
DB_PASS, secret: selezionadb-passe versione:latest - Aggiungi nome:
DB_NAME, secret: selezionadb-namee versione:latest
- Aggiungi nome:
- Variabili di ambiente, aggiungi le seguenti variabili facendo clic sul pulsante + Aggiungi variabile:
- Scheda Impostazioni:

Lascia invariati gli altri valori predefiniti.
👉 Fai clic su CREA.
Cloud Run eseguirà il deployment del servizio.
Una volta eseguito il deployment, se non ti trovi già nella pagina dei dettagli, fai clic sul nome del servizio per passare alla relativa pagina. Puoi trovare l'URL di deployment disponibile in alto.

👉 Nell'interfaccia dell'applicazione, seleziona 7 per l'anno, scegli Mathematics come oggetto e inserisci Algebra nel campo Richiesta di componente aggiuntivo.
👉 Fai clic su Genera piano. In questo modo, l'agente avrà il contesto necessario per generare un piano di lezioni personalizzato.
Complimenti! Hai creato correttamente un piano di insegnamento utilizzando il nostro potente agente AI. Ciò dimostra il potenziale degli agenti di ridurre significativamente il carico di lavoro e semplificare le attività, migliorando in definitiva l'efficienza e semplificando la vita degli insegnanti.
9. Sistemi multi-agente
Ora che abbiamo implementato correttamente lo strumento di creazione del piano di insegnamento, concentriamoci sulla creazione del portale per gli studenti. Questo portale fornirà agli studenti l'accesso a quiz, riepiloghi audio e compiti relativi al loro corso. Data la portata di questa funzionalità, sfrutteremo la potenza dei sistemi multi-agente per creare una soluzione modulare e scalabile.
Come abbiamo discusso in precedenza, anziché fare affidamento su un singolo agente per gestire tutto, un sistema multi-agente ci consente di suddividere il workload in attività più piccole e specializzate, ciascuna gestita da un agente dedicato. Questo approccio offre diversi vantaggi chiave:
Modularità e manutenibilità: anziché creare un singolo agente che fa tutto, crea agenti più piccoli e specializzati con responsabilità ben definite. Questa modularità rende il sistema più facile da comprendere, gestire e sottoporre a debug. Quando si verifica un problema, puoi isolarlo in un agente specifico, anziché dover esaminare un codebase enorme.
Scalabilità: scalare un singolo agente complesso può essere un collo di bottiglia. Con un sistema multi-agente, puoi scalare i singoli agenti in base alle loro esigenze specifiche. Ad esempio, se un agente gestisce un volume elevato di richieste, puoi facilmente avviare più istanze di quell'agente senza influire sul resto del sistema.
Specializzazione del team: pensa a questo: non chiederesti a un ingegnere di creare un'intera applicazione da zero. Al contrario, metti insieme un team di specialisti, ognuno con competenze in un'area particolare. Allo stesso modo, un sistema multi-agente ti consente di sfruttare i punti di forza di diversi LLM e strumenti, assegnandoli ad agenti più adatti a compiti specifici.
Sviluppo parallelo: team diversi possono lavorare contemporaneamente su agenti diversi, velocizzando il processo di sviluppo. Poiché gli agenti sono indipendenti, è meno probabile che le modifiche apportate a un agente influiscano sugli altri.
Architettura basata su eventi
Per consentire una comunicazione e un coordinamento efficaci tra questi agenti, utilizzeremo un'architettura basata sugli eventi. Ciò significa che gli agenti reagiranno agli "eventi" che si verificano all'interno del sistema.
Gli agenti si iscrivono a tipi di eventi specifici (ad es. piano didattico generato, compito creato). Quando si verifica un evento, gli agenti pertinenti vengono avvisati e possono reagire di conseguenza. Questo disaccoppiamento favorisce flessibilità, scalabilità e reattività in tempo reale.

Per iniziare, abbiamo bisogno di un modo per trasmettere questi eventi. Per farlo, configureremo un argomento Pub/Sub. Iniziamo creando un argomento chiamato piano.
👉 Vai a Pub/Sub nella console Google Cloud.
👉 Fai clic sul pulsante Crea argomento.
👉 Configura l'argomento con ID/nome plan e deseleziona Add a default subscription, lascia il resto come predefinito e fai clic su Crea.
La pagina Pub/Sub viene aggiornata e ora dovresti vedere l'argomento appena creato elencato nella tabella. 
Ora integriamo la funzionalità di pubblicazione degli eventi Pub/Sub nel nostro agente di pianificazione. Aggiungeremo un nuovo strumento che invia un evento "plan " all'argomento Pub/Sub che abbiamo appena creato. Questo evento segnalerà ad altri agenti del sistema (come quelli del portale per gli studenti) che è disponibile un nuovo piano di insegnamento.
👉 Torna all'editor di Cloud Code e apri il file app.py che si trova nella cartella planner. Aggiungeremo una funzione che pubblica l'evento. Sostituisci:
##ADD SEND PLAN EVENT FUNCTION HERE
con il seguente codice
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: questa funzione prende come input il piano di insegnamento generato, crea un client publisher Pub/Sub, costruisce il percorso dell'argomento, converte il piano di insegnamento in una stringa JSON e pubblica il messaggio nell'argomento.
Nello stesso file app.py
👉 Aggiorna il prompt per indicare all'agente di inviare l'evento del piano di insegnamento all'argomento Pub/Sub dopo aver generato il piano di insegnamento. *Sostituisci
### ADD send_plan_event CALL
con quanto segue:
send_plan_event(teaching_plan)
Aggiungendo lo strumento send_plan_event e modificando il prompt, abbiamo consentito al nostro agente di pianificazione di pubblicare eventi su Pub/Sub, consentendo ad altri componenti del nostro sistema di reagire alla creazione di nuovi piani didattici. Nelle sezioni seguenti avremo un sistema multi-agente funzionale.
10. Aiutare gli studenti con i quiz on demand
Immagina un ambiente di apprendimento in cui gli studenti hanno accesso a una fornitura illimitata di quiz personalizzati in base ai loro piani di apprendimento specifici. Questi quiz forniscono un feedback immediato, incluse risposte e spiegazioni, favorendo una comprensione più approfondita del materiale. Questo è il potenziale che miriamo a sbloccare con il nostro portale di quiz basato sull'AI.
Per dare vita a questa visione, creeremo un componente di generazione di quiz in grado di creare domande a scelta multipla in base ai contenuti del piano didattico.

👉 Nel riquadro Explorer dell'editor di Cloud Code, vai alla cartella portal. Apri la copia del file quiz.py e incolla il seguente codice alla fine del file.
def generate_quiz_question(file_name: str, difficulty: str, region:str ):
"""Generates a single multiple-choice quiz question using the LLM.
```json
{
"question": "The question itself",
"options": ["Option A", "Option B", "Option C", "Option D"],
"answer": "The correct answer letter (A, B, C, or D)"
}
```
"""
print(f"region: {region}")
# Connect to resourse needed from Google Cloud
llm = VertexAI(model_name="gemini-2.5-flash-preview-04-17", location=region)
plan=None
#load the file using file_name and read content into string call plan
with open(file_name, 'r') as f:
plan = f.read()
parser = JsonOutputParser(pydantic_object=QuizQuestion)
instruction = f"You'll provide one question with difficulty level of {difficulty}, 4 options as multiple choices and provide the anwsers, the quiz needs to be related to the teaching plan {plan}"
prompt = PromptTemplate(
template="Generates a single multiple-choice quiz question\n {format_instructions}\n {instruction}\n",
input_variables=["instruction"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
chain = prompt | llm | parser
response = chain.invoke({"instruction": instruction})
print(f"{response}")
return response
Nell'agente crea un parser di output JSON progettato appositamente per comprendere e strutturare l'output dell'LLM. Utilizza il modello QuizQuestion che abbiamo definito in precedenza per garantire che l'output analizzato sia conforme al formato corretto (domanda, opzioni e risposta).
👉 Nel terminale, esegui i seguenti comandi per configurare un ambiente virtuale, installare le dipendenze e avviare l'agente:
gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py
👉 Dal menu "Anteprima web" nell'angolo in alto a destra, scegli Anteprima sulla porta 8080. Cloud Shell aprirà una nuova scheda o finestra del browser con l'anteprima web della tua applicazione.
👉 Nell'applicazione web, fai clic sul link "Quiz" nella barra di navigazione in alto o nella scheda della pagina indice. Dovresti visualizzare tre quiz generati in modo casuale per lo studente. Questi quiz si basano sul piano didattico e dimostrano la potenza del nostro sistema di generazione di quiz basato sull'AI.

👉 Per interrompere il processo in esecuzione in locale, premi Ctrl+C nel terminale.
Gemini 2 Thinking for Explanations
Ok, quindi abbiamo i quiz, che sono un ottimo inizio. Ma cosa succede se gli studenti sbagliano qualcosa? È qui che avviene il vero apprendimento, giusto? Se riusciamo a spiegare perché la risposta non era corretta e come arrivare a quella giusta, è molto più probabile che la ricordino. Inoltre, aiuta a chiarire eventuali dubbi e a rafforzare la loro fiducia.
Ecco perché useremo l'artiglieria pesante: il modello "Ragionamento" di Gemini 2. È come dare all'AI un po' di tempo in più per riflettere prima di spiegare. In questo modo, può fornire un feedback più dettagliato e migliore.
Vogliamo vedere se può aiutare gli studenti fornendo assistenza, rispondendo alle domande e fornendo spiegazioni dettagliate. Per provarlo, inizieremo con un argomento notoriamente difficile, il calcolo.

👉 Innanzitutto, vai all'editor di Cloud Code, in answer.py all'interno della cartella portal. Sostituisci il seguente codice della funzione
def answer_thinking(question, options, user_response, answer, region):
return ""
con il seguente snippet di codice:
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)
Si tratta di un'app langchain molto semplice che inizializza il modello Gemini 2 Flash, a cui viene chiesto di agire come un insegnante utile e di fornire spiegazioni
👉 Esegui questo comando nel terminale:
gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python answer.py
Dovresti visualizzare un output simile all'esempio fornito nelle istruzioni originali. Il modello attuale potrebbe non fornire una spiegazione esaustiva.
Okay, I see the question and the choices. The question is to evaluate the limit:
lim (x→0) [(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!
👉 Nel file answer.py, sostituisci
model_name da gemini-2.0-flash-001 a gemini-2.0-flash-thinking-exp-01-21 nella funzione answer_thinking.
In questo modo, l'LLM viene sostituito con un altro che funziona meglio con il ragionamento. In questo modo, il modello potrà generare spiegazioni migliori.
👉 Esegui di nuovo lo script answer.py per testare il nuovo modello pensante:
gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python answer.py
Ecco un esempio di risposta del modello di ragionamento molto più completa e dettagliata, che fornisce una spiegazione passo passo su come risolvere il problema di calcolo. Ciò evidenzia la potenza dei modelli di "pensiero" nella generazione di spiegazioni di alta qualità. Dovresti visualizzare un output simile a questo:
Hey there! Let's take a look at this limit problem together. You were asked to evaluate:
lim (x→0) [(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 (x→0) [ -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.
👉RIMUOVI il seguente codice di test da 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)
👉 Esegui questi comandi nel terminale per configurare un ambiente virtuale, installare le dipendenze e avviare l'agente:
gcloud config set project $(cat ~/project_id.txt)
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python app.py
👉 Dal menu "Anteprima web" nell'angolo in alto a destra, scegli Anteprima sulla porta 8080. Cloud Shell aprirà una nuova scheda o finestra del browser con l'anteprima web della tua applicazione.
👉 Nell'applicazione web, fai clic sul link "Quiz" nella barra di navigazione in alto o nella scheda della pagina indice.
👉 Rispondi a tutti i quiz e assicurati di sbagliare almeno una risposta, quindi fai clic su Invia.

Anziché fissare il vuoto in attesa della risposta, passa al terminale di Cloud Editor. Puoi osservare l'avanzamento e qualsiasi output o messaggio di errore generato dalla funzione nel terminale dell'emulatore. 😁
👉 Nel terminale, interrompi il processo in esecuzione in locale premendo Ctrl+C nel terminale.
11. (FACOLTATIVO) Orchestrazione degli agenti con Eventarc
Finora, il portale per gli studenti ha generato quiz basati su un insieme predefinito di piani didattici. È utile, ma significa che l'agente di pianificazione e l'agente di quiz del portale non comunicano tra loro. Ricordi quando abbiamo aggiunto la funzionalità in cui l'agente di pianificazione pubblica i piani di insegnamento appena generati in un argomento Pub/Sub? Ora è il momento di collegarlo al nostro agente del portale.

Vogliamo che il portale aggiorni automaticamente i contenuti dei quiz ogni volta che viene generato un nuovo piano didattico. A questo scopo, creeremo un endpoint nel portale in grado di ricevere questi nuovi piani.
👉 Nel riquadro Explorer dell'editor di Cloud Code, vai alla cartella portal.
👉 Apri il file app.py per modificarlo. Sostituisci la riga REPLACE ## REPLACE ME! NEW TEACHING PLAN con il seguente codice:
@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
Ricreazione ed esecuzione del deployment in Cloud Run
Dovrai aggiornare e ridistribuire sia l'agente di pianificazione sia l'agente del portale in Cloud Run. In questo modo, avranno il codice più recente e saranno configurati per comunicare tramite gli eventi.

👉 Per prima cosa, ricompiliamo e inviamo l'immagine dell'agente planner. Nel terminale esegui:
cd ~/aidemy-bootstrap/planner/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
👉 Faremo lo stesso, creeremo ed eseguiremo il push dell'immagine dell'agente portal:
cd ~/aidemy-bootstrap/portal/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
👉 Vai ad Artifact Registry. Dovresti vedere le immagini container aidemy-planner e aidemy-portal elencate in agent-repository.

👉 Torna al terminale ed esegui questo comando per aggiornare l'immagine Cloud Run per l'agente di pianificazione:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-planner \
--region=us-central1 \
--image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner:latest
Dovresti visualizzare un output simile a questo:
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
Prendi nota dell'URL del servizio, ovvero il link all'agente di pianificazione di cui è stato eseguito il deployment. Se in un secondo momento devi determinare l'URL del servizio dell'agente di pianificazione, utilizza questo comando:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services describe aidemy-planner \
--region=us-central1 \
--format 'value(status.url)'
👉 Esegui questo comando per creare l'istanza Cloud Run per l'agente del portale
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run deploy aidemy-portal \
--image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal:latest \
--region=us-central1 \
--platform=managed \
--allow-unauthenticated \
--memory=2Gi \
--cpu=2 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID}
Dovresti visualizzare un output simile a questo:
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
Prendi nota dell'URL del servizio, ovvero il link al portale per gli studenti di cui hai eseguito il deployment. Se in un secondo momento devi determinare l'URL del servizio del portale per gli studenti, utilizza questo comando:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services describe aidemy-portal \
--region=us-central1 \
--format 'value(status.url)'
Creazione del trigger Eventarc
Ma ecco la domanda più importante: come viene avvisato questo endpoint quando c'è un nuovo piano in attesa nell'argomento Pub/Sub? È qui che entra in gioco Eventarc per salvare la situazione.
Eventarc funge da ponte, ascoltando eventi specifici (come l'arrivo di un nuovo messaggio nell'argomento Pub/Sub) e attivando automaticamente le azioni in risposta. Nel nostro caso, rileverà la pubblicazione di un nuovo piano didattico e invierà un segnale all'endpoint del nostro portale, comunicando che è il momento di eseguire l'aggiornamento.
Con Eventarc che gestisce la comunicazione basata su eventi, possiamo connettere facilmente l'agente di pianificazione e l'agente del portale, creando un sistema di apprendimento davvero dinamico e reattivo. È come avere un messenger intelligente che invia automaticamente i piani di lezioni più recenti nel posto giusto.
👉 Nella console, vai a Eventarc.
👉 Fai clic sul pulsante "+ CREA TRIGGER".
Configura il trigger (nozioni di base):
- Nome trigger:
plan-topic-trigger - Tipo di trigger: origini Google
- Fornitore di eventi: Cloud Pub/Sub
- Tipo di evento:
google.cloud.pubsub.topic.v1.messagePublished - Argomento Cloud Pub/Sub: seleziona
projects/PROJECT_ID/topics/plan - Regione:
us-central1. - Service account:
- CONCEDI all'account di servizio il ruolo
roles/iam.serviceAccountTokenCreator - Utilizza il valore predefinito: service account Compute predefinito
- CONCEDI all'account di servizio il ruolo
- Destinazione evento: Cloud Run
- Servizio Cloud Run:
aidemy-portal - Ignora il messaggio di errore: autorizzazione negata per "locations/me-central2" (oppure la risorsa potrebbe non esistere).
- Percorso URL del servizio:
/new_teaching_plan
👉 Fai clic su "Crea".
La pagina Trigger Eventarc viene aggiornata e ora dovresti vedere il trigger appena creato elencato nella tabella.
Ora accedi all'agente planner utilizzando il relativo URL del servizio per richiedere un nuovo piano di insegnamento.
👉 Esegui questo comando nel terminale per determinare l'URL del servizio dell'agente di pianificazione:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner
👉 Vai all'URL di output e questa volta prova Anno 5, Materia Science e Richiesta di componenti aggiuntivi atoms.
Poi, attendi un minuto o due. Questo ritardo è stato introdotto a causa della limitazione di fatturazione di questo lab. In condizioni normali, non dovrebbe esserci alcun ritardo.
Infine, accedi al portale dello studente utilizzando il relativo URL del servizio.
Esegui questo comando nel terminale per determinare l'URL del servizio dell'agente del portale per gli studenti:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal
Dovresti vedere che i quiz sono stati aggiornati e ora sono in linea con il nuovo piano didattico che hai appena generato. Ciò dimostra l'integrazione riuscita di Eventarc nel sistema Aidemy.

Complimenti! Hai creato un sistema multi-agente su Google Cloud, sfruttando l'architettura basata sugli eventi per una maggiore scalabilità e flessibilità. Hai creato una base solida, ma c'è ancora molto da esplorare. Per approfondire i vantaggi reali di questa architettura, scoprire la potenza dell'API Multimodal Live di Gemini 2 e imparare a implementare l'orchestrazione a percorso singolo con LangGraph, continua a leggere i prossimi due capitoli.
12. (FACOLTATIVO) Riepiloghi audio con Gemini
Gemini può comprendere ed elaborare informazioni provenienti da varie fonti, come testo, immagini e persino audio, aprendo una gamma completamente nuova di possibilità per l'apprendimento e la creazione di contenuti. La capacità di Gemini di "vedere", "ascoltare" e "leggere" offre esperienze utente creative e coinvolgenti.
Oltre a creare immagini o testo, un altro passaggio importante nell'apprendimento è il riepilogo efficace. Pensaci: quante volte ricordi più facilmente il testo di una canzone orecchiabile rispetto a qualcosa che hai letto in un libro di testo? Il suono può essere incredibilmente memorabile. Ecco perché sfrutteremo le funzionalità multimodali di Gemini per generare riepiloghi audio dei nostri piani didattici. In questo modo, gli studenti avranno a disposizione un modo comodo e coinvolgente per rivedere il materiale, il che potrebbe migliorare la memorizzazione e la comprensione grazie al potere dell'apprendimento uditivo.

Abbiamo bisogno di un posto dove archiviare i file audio generati. Cloud Storage offre una soluzione scalabile e affidabile.
👉 Vai a Storage nella console. Fai clic su "Bucket" nel menu a sinistra. Fai clic sul pulsante "+ CREA" in alto.
👉 Configura il nuovo bucket:
- nome bucket:
aidemy-recap-UNIQUE_NAME.- IMPORTANTE: assicurati di definire un nome bucket univoco che inizi con
aidemy-recap-. Questo prefisso univoco è fondamentale per evitare conflitti di denominazione durante la creazione del bucket Cloud Storage.
- IMPORTANTE: assicurati di definire un nome bucket univoco che inizi con
- regione:
us-central1. - Classe di archiviazione: "Standard". Standard è adatta ai dati a cui si accede di frequente.
- Controllo dell'accesso: lascia selezionato il controllo dell'accesso predefinito "Uniforme". In questo modo, il controllo dell'accesso a livello di bucket è coerente.
- Opzioni avanzate: per questo workshop, le impostazioni predefinite sono in genere sufficienti.
Fai clic sul pulsante CREA per creare il bucket.
- Potresti visualizzare un popup relativo alla prevenzione dell'accesso pubblico. Lascia selezionata la casella "Applica la prevenzione dell'accesso pubblico in questo bucket" e fai clic su
Confirm.
Ora vedrai il bucket appena creato nell'elenco Bucket. Ricorda il nome del bucket, ti servirà in seguito.
👉 Nel terminale dell'editor di Cloud Code, esegui questi comandi per concedere al service account l'accesso al bucket:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectCreator"
👉 Nell'editor di Cloud Code, apri audio.py all'interno della cartella courses. Incolla il seguente codice alla fine del file:
config = LiveConnectConfig(
response_modalities=["AUDIO"],
speech_config=SpeechConfig(
voice_config=VoiceConfig(
prebuilt_voice_config=PrebuiltVoiceConfig(
voice_name="Charon",
)
)
),
)
async def process_weeks(teaching_plan: str):
region = "us-east5" #To workaround onRamp quota limits
client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
clientAudio = genai.Client(vertexai=True, project=PROJECT_ID, location="us-central1")
async with clientAudio.aio.live.connect(
model=MODEL_ID,
config=config,
) as session:
for week in range(1, 4):
response = client.models.generate_content(
model="gemini-2.0-flash-001",
contents=f"Given the following teaching plan: {teaching_plan}, Extrace content plan for week {week}. And return just the plan, nothingh else " # Clarified prompt
)
prompt = f"""
Assume you are the instructor.
Prepare a concise and engaging recap of the key concepts and topics covered.
This recap should be suitable for generating a short audio summary for students.
Focus on the most important learnings and takeaways, and frame it as a direct address to the students.
Avoid overly formal language and aim for a conversational tone, tell a few jokes.
Teaching plan: {response.text} """
print(f"prompt --->{prompt}")
await session.send(input=prompt, end_of_turn=True)
with open(f"temp_audio_week_{week}.raw", "wb") as temp_file:
async for message in session.receive():
if message.server_content.model_turn:
for part in message.server_content.model_turn.parts:
if part.inline_data:
temp_file.write(part.inline_data.data)
data, samplerate = sf.read(f"temp_audio_week_{week}.raw", channels=1, samplerate=24000, subtype='PCM_16', format='RAW')
sf.write(f"course-week-{week}.wav", data, samplerate)
storage_client = storage.Client()
bucket = storage_client.bucket(BUCKET_NAME)
blob = bucket.blob(f"course-week-{week}.wav") # Or give it a more descriptive name
blob.upload_from_filename(f"course-week-{week}.wav")
print(f"Audio saved to GCS: gs://{BUCKET_NAME}/course-week-{week}.wav")
await session.close()
def breakup_sessions(teaching_plan: str):
asyncio.run(process_weeks(teaching_plan))
- Connessione di streaming: innanzitutto, viene stabilita una connessione persistente con l'endpoint dell'API Live. A differenza di una chiamata API standard in cui invii una richiesta e ricevi una risposta, questa connessione rimane aperta per uno scambio continuo di dati.
- Configurazione multimodale: utilizza la configurazione per specificare il tipo di output che vuoi (in questo caso, audio) e puoi anche specificare i parametri che vuoi utilizzare (ad es.selezione della voce, codifica audio).
- Elaborazione asincrona: questa API funziona in modo asincrono, il che significa che non blocca il thread principale durante l'attesa del completamento della generazione dell'audio. Elaborando i dati in tempo reale e inviando l'output in blocchi, offre un'esperienza quasi istantanea.
Ora la domanda chiave è: quando deve essere eseguito questo processo di generazione dell'audio? Idealmente, vogliamo che i riepiloghi audio siano disponibili non appena viene creato un nuovo piano di insegnamento. Poiché abbiamo già implementato un'architettura basata su eventi pubblicando il piano didattico in un argomento Pub/Sub, possiamo semplicemente sottoscrivere l'argomento.
Tuttavia, non generiamo nuovi piani didattici molto spesso. Non sarebbe efficiente avere un agente in esecuzione e in attesa costante di nuovi piani. Ecco perché ha perfettamente senso implementare questa logica di generazione audio come funzione Cloud Run.
Se lo implementi come funzione, rimane inattivo finché non viene pubblicato un nuovo messaggio nell'argomento Pub/Sub. Quando ciò accade, la funzione viene attivata automaticamente, genera i riepiloghi audio e li memorizza nel nostro bucket.
👉 Nella cartella courses del file main.py, questo file definisce la funzione Cloud Run che verrà attivata quando sarà disponibile un nuovo piano di insegnamento. Riceve il piano e avvia la generazione del riepilogo audio. Aggiungi il seguente snippet di codice alla fine del file.
@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: questo decoratore contrassegna la funzione come funzione Cloud Run che verrà attivata da CloudEvents.
Test locale
👉 Eseguiamo questa operazione in un ambiente virtuale e installiamo le librerie Python necessarie per la funzione Cloud Run.
cd ~/aidemy-bootstrap/courses
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
python -m venv env
source env/bin/activate
pip install -r requirements.txt
👉 L'emulatore di Cloud Run Functions ci consente di testare la funzione localmente prima di eseguirne il deployment su Google Cloud. Avvia un emulatore locale eseguendo:
functions-framework --target process_teaching_plan --signature-type=cloudevent --source main.py
👉 Mentre l'emulatore è in esecuzione, puoi inviare CloudEvent di test all'emulatore per simulare la pubblicazione di un nuovo piano didattico. In un nuovo terminale:

👉 Esegui:
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=="
}
}'
Anziché fissare il vuoto in attesa della risposta, passa all'altro terminale Cloud Shell. Puoi osservare l'avanzamento e qualsiasi output o messaggio di errore generato dalla funzione nel terminale dell'emulatore. 😁
Nel secondo terminale dovresti vedere che è stato restituito OK.
👉 Per verificare i dati nel bucket, vai a Cloud Storage, seleziona la scheda "Bucket" e poi aidemy-recap-UNIQUE_NAME.

👉 Nel terminale in cui è in esecuzione l'emulatore, digita ctrl+c per uscire. e chiudi il secondo terminale. Chiudi il secondo terminale ed esegui deactivate per uscire dall'ambiente virtuale.
deactivate
Deployment su Google Cloud
👉 Dopo aver eseguito i test localmente, è il momento di eseguire il deployment dell'agente del corso su Google Cloud. Nel terminale, esegui questi comandi:
cd ~/aidemy-bootstrap/courses
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud functions deploy courses-agent \
--region=us-central1 \
--gen2 \
--source=. \
--runtime=python312 \
--trigger-topic=plan \
--entry-point=process_teaching_plan \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
Verifica il deployment andando su Cloud Run nella console Google Cloud.Dovresti visualizzare un nuovo servizio denominato courses-agent.

Per controllare la configurazione del trigger, fai clic sul servizio courses-agent per visualizzarne i dettagli. Vai alla scheda "TRIGGER".
Dovresti vedere un trigger configurato per ascoltare i messaggi pubblicati nell'argomento del piano.

Infine, vediamo come funziona end-to-end.
👉 Dobbiamo configurare l'agente del portale in modo che sappia dove trovare i file audio generati. Nel terminale, esegui:
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-portal \
--region=us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
👉 Prova a generare un nuovo piano didattico utilizzando la pagina web dell'agente di pianificazione. L'avvio potrebbe richiedere alcuni minuti, non preoccuparti, è un servizio serverless.
Per accedere all'agente di pianificazione, ottieni il relativo URL del servizio eseguendo questo comando nel terminale:
gcloud run services list \
--platform=managed \
--region=us-central1 \
--format='value(URL)' | grep planner
Dopo aver generato il nuovo piano, attendi 2-3 minuti per la generazione dell'audio. Anche in questo caso, l'operazione richiederà qualche minuto in più a causa della limitazione di fatturazione con questo account lab.
Puoi monitorare se la funzione courses-agent ha ricevuto il piano di insegnamento controllando la scheda "TRIGGER" della funzione. Aggiorna periodicamente la pagina. Alla fine, dovresti vedere che la funzione è stata richiamata. Se la funzione non è stata richiamata dopo più di 2 minuti, puoi provare a generare di nuovo il piano di insegnamento. Tuttavia, evita di generare piani ripetutamente in rapida successione, poiché ogni piano generato verrà consumato ed elaborato in sequenza dall'agente, creando potenzialmente un backlog.

👉 Visita il portale e fai clic su "Corsi". Dovresti vedere tre schede, ognuna con un riepilogo audio. Per trovare l'URL dell'agente del portale:
gcloud run services list \
--platform=managed \
--region=us-central1 \
--format='value(URL)' | grep portal
Fai clic su "Riproduci" su ogni corso per assicurarti che i riepiloghi audio siano allineati al piano didattico che hai appena generato. 
Esci dall'ambiente virtuale.
deactivate
13. (FACOLTATIVO) Collaborazione basata sui ruoli con Gemini e DeepSeek
Avere più punti di vista è fondamentale, soprattutto quando si creano compiti coinvolgenti e ponderati. Ora creeremo un sistema multi-agente che sfrutta due modelli diversi con ruoli distinti per generare compiti: uno promuove la collaborazione e l'altro incoraggia lo studio autonomo. Utilizzeremo un'architettura "single-shot", in cui il flusso di lavoro segue un percorso fisso.
Generatore di compiti Gemini
Inizieremo configurando la funzione Gemini per generare compiti con un'enfasi sulla collaborazione. Modifica il file gemini.py che si trova nella cartella assignment.
👉 Incolla il seguente codice alla fine del file 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()
Utilizza il modello Gemini per generare compiti.
Siamo pronti a testare Gemini Agent.
👉 Esegui questi comandi nel terminale per configurare l'ambiente:
cd ~/aidemy-bootstrap/assignment
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
python -m venv env
source env/bin/activate
pip install -r requirements.txt
👉 Puoi eseguire il test:
python gemini.py
Nell'output dovresti vedere un compito che prevede più lavoro di gruppo. Anche il test di asserzione alla fine restituirà i risultati.
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.
....
Interrompi con ctl+c e per pulire il codice di test. RIMUOVI il seguente codice da 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()
Configurare il generatore di compiti DeepSeek
Sebbene le piattaforme AI basate su cloud siano pratiche, l'hosting autonomo di LLM può essere fondamentale per proteggere la privacy dei dati e garantire la sovranità dei dati. Eseguiremo il deployment del modello DeepSeek più piccolo (1,5 miliardi di parametri) su un'istanza di Cloud Compute Engine. Esistono altri modi, ad esempio l'hosting sulla piattaforma Vertex AI di Google o sulla tua istanza GKE, ma dato che questo è solo un workshop sugli agenti AI e non voglio trattenerti qui per sempre, utilizziamo il modo più semplice. Se ti interessa e vuoi approfondire altre opzioni, dai un'occhiata al file deepseek-vertexai.py nella cartella dell'assegnazione, dove viene fornito un codice di esempio su come interagire con i modelli di cui è stato eseguito il deployment su Vertex AI.

👉 Esegui questo comando nel terminale per creare una piattaforma LLM autogestita Ollama:
cd ~/aidemy-bootstrap/assignment
gcloud config set project $(cat ~/project_id.txt)
gcloud compute instances create ollama-instance \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--machine-type=e2-standard-4 \
--zone=us-central1-a \
--metadata-from-file startup-script=startup.sh \
--boot-disk-size=50GB \
--tags=ollama \
--scopes=https://www.googleapis.com/auth/cloud-platform
Per verificare che l'istanza Compute Engine sia in esecuzione:
Vai a Compute Engine > "Istanze VM" nella console Google Cloud. Dovresti vedere ollama-instance elencato con un segno di spunta verde che indica che è in esecuzione. Se non lo vedi, assicurati che la zona sia us-central1. In caso contrario, potresti doverlo cercare.

👉 Installeremo il modello DeepSeek più piccolo e lo testeremo. Nell'editor di Cloud Shell, in un terminale Nuovo, esegui questo comando per connetterti all'istanza GCE tramite SSH.
gcloud compute ssh ollama-instance --zone=us-central1-a
Una volta stabilita la connessione SSH, potrebbe essere visualizzato il seguente messaggio:
"Vuoi continuare (Y/n)?"
Digita Y(senza distinzione tra maiuscole e minuscole) e premi Invio per continuare.
Successivamente, potrebbe esserti chiesto di creare una passphrase per la chiave SSH. Se preferisci non utilizzare una passphrase, premi Invio due volte per accettare il valore predefinito (nessuna passphrase).
👉 Ora che ti trovi nella macchina virtuale, estrai il modello DeepSeek R1 più piccolo e verifica se funziona.
ollama pull deepseek-r1:1.5b
ollama run deepseek-r1:1.5b "who are you?"
👉 Esci dall'istanza GCE inserendo quanto segue nel terminale SSH:
exit
👉 Successivamente, configura la policy di rete in modo che altri servizi possano accedere al modello LLM. Limita l'accesso all'istanza se vuoi farlo per la produzione, implementa l'accesso di sicurezza per il servizio o limita l'accesso IP. Esegui:
gcloud compute firewall-rules create allow-ollama-11434 \
--allow=tcp:11434 \
--target-tags=ollama \
--description="Allow access to Ollama on port 11434"
👉 Per verificare se la policy firewall funziona correttamente, prova a eseguire:
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
}'
Successivamente, lavoreremo sulla funzione Deepseek nell'agente dei compiti per generare compiti con enfasi sul lavoro individuale.
👉 Modifica deepseek.py nella cartella assignment aggiungendo il seguente snippet alla fine:
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()
👉 Proviamo a eseguire:
cd ~/aidemy-bootstrap/assignment
source env/bin/activate
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
python deepseek.py
Dovresti vedere un compito che prevede più lavoro di studio autonomo.
**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.
....
👉 Interrompi ctl+c e pulisci il codice di test. RIMUOVI il seguente codice da 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()
Ora utilizzeremo lo stesso modello Gemini per combinare entrambi gli incarichi in uno nuovo. Modifica il file gemini.py che si trova nella cartella assignment.
👉 Incolla il seguente codice alla fine del file 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
Per combinare i punti di forza di entrambi i modelli, orchestreremo un flusso di lavoro definito utilizzando LangGraph. Questo flusso di lavoro è composto da tre passaggi: innanzitutto, il modello Gemini genera un compito incentrato sulla collaborazione; in secondo luogo, il modello DeepSeek genera un compito che enfatizza il lavoro individuale; infine, Gemini sintetizza questi due compiti in un unico compito completo. Poiché predefiniamo la sequenza di passaggi senza il processo decisionale del modello LLM, si tratta di un'orchestrazione a percorso singolo definita dall'utente.

👉 Incolla il seguente codice alla fine del file main.py nella cartella assignment:
def create_assignment(teaching_plan: str):
print(f"create_assignment---->{teaching_plan}")
builder = StateGraph(State)
builder.add_node("gen_assignment_gemini", gen_assignment_gemini)
builder.add_node("gen_assignment_deepseek", gen_assignment_deepseek)
builder.add_node("combine_assignments", combine_assignments)
builder.add_edge(START, "gen_assignment_gemini")
builder.add_edge("gen_assignment_gemini", "gen_assignment_deepseek")
builder.add_edge("gen_assignment_deepseek", "combine_assignments")
builder.add_edge("combine_assignments", END)
graph = builder.compile()
state = graph.invoke({"teaching_plan": teaching_plan})
return state["final_assignment"]
import unittest
class TestCreateAssignment(unittest.TestCase):
def test_create_assignment(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = create_assignment(initial_state)
print(updated_state)
if __name__ == '__main__':
unittest.main()
👉 Per testare inizialmente la funzione create_assignment e verificare che il flusso di lavoro che combina Gemini e DeepSeek sia funzionante, esegui questo comando:
cd ~/aidemy-bootstrap/assignment
source env/bin/activate
pip install -r requirements.txt
python main.py
Dovresti vedere qualcosa che combini entrambi i modelli con la loro prospettiva individuale per lo studio degli studenti e anche per i lavori di gruppo.
**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
👉 Interrompi ctl+c e pulisci il codice di test. RIMUOVI il seguente codice da main.py
import unittest
class TestCreateAssignment(unittest.TestCase):
def test_create_assignment(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = create_assignment(initial_state)
print(updated_state)
if __name__ == '__main__':
unittest.main()

Per rendere automatico il processo di generazione dei compiti e reattivo ai nuovi piani didattici, sfrutteremo l'architettura basata sugli eventi esistente. Il codice seguente definisce una funzione Cloud Run (generate_assignment) che verrà attivata ogni volta che un nuovo piano didattico viene pubblicato nell'argomento Pub/Sub "plan".
👉 Aggiungi il seguente codice alla fine di main.py nella cartella assignment:
@functions_framework.cloud_event
def generate_assignment(cloud_event):
print(f"CloudEvent received: {cloud_event.data}")
try:
if isinstance(cloud_event.data.get('message', {}).get('data'), str):
data = json.loads(base64.b64decode(cloud_event.data['message']['data']).decode('utf-8'))
teaching_plan = data.get('teaching_plan')
elif 'teaching_plan' in cloud_event.data:
teaching_plan = cloud_event.data["teaching_plan"]
else:
raise KeyError("teaching_plan not found")
assignment = create_assignment(teaching_plan)
print(f"Assignment---->{assignment}")
#Store the return assignment into bucket as a text file
storage_client = storage.Client()
bucket = storage_client.bucket(ASSIGNMENT_BUCKET)
file_name = f"assignment-{random.randint(1, 1000)}.txt"
blob = bucket.blob(file_name)
blob.upload_from_string(assignment)
return f"Assignment generated and stored in {ASSIGNMENT_BUCKET}/{file_name}", 200
except (json.JSONDecodeError, AttributeError, KeyError) as e:
print(f"Error decoding CloudEvent data: {e} - Data: {cloud_event.data}")
return "Error processing event", 500
except Exception as e:
print(f"Error generate assignment: {e}")
return "Error generate assignment", 500
Test locale
Prima di eseguire il deployment su Google Cloud, è consigliabile testare la funzione Cloud Run localmente. Ciò consente un'iterazione più rapida e un debug più semplice.
Per prima cosa, crea un bucket Cloud Storage per archiviare i file degli esercizi generati e concedi al service account l'accesso al bucket. Esegui questi comandi nel terminale:
👉IMPORTANTE: assicurati di definire un nome ASSIGNMENT_BUCKET univoco che inizi con "aidemy-assignment-". Questo nome univoco è fondamentale per evitare conflitti di denominazione durante la creazione del bucket Cloud Storage. (Sostituisci <YOUR_NAME> con una parola casuale)
export ASSIGNMENT_BUCKET=aidemy-assignment-<YOUR_NAME> #Name must be unqiue
👉 Esegui:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gsutil mb -p $PROJECT_ID -l us-central1 gs://$ASSIGNMENT_BUCKET
gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectCreator"
👉 Ora avvia l'emulatore di Cloud Run Functions:
cd ~/aidemy-bootstrap/assignment
functions-framework \
--target generate_assignment \
--signature-type=cloudevent \
--source main.py
👉 Mentre l'emulatore è in esecuzione in un terminale, apri un secondo terminale in Cloud Shell. Nel secondo terminale, invia un CloudEvent di test all'emulatore per simulare la pubblicazione di un nuovo piano didattico:

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=="
}
}'
Anziché fissare il vuoto in attesa della risposta, passa all'altro terminale Cloud Shell. Puoi osservare l'avanzamento e qualsiasi output o messaggio di errore generato dalla funzione nel terminale dell'emulatore. 😁
Il comando curl deve stampare "OK" (senza un carattere di nuova riga, quindi "OK" potrebbe apparire sulla stessa riga del prompt della shell del terminale).
Per verificare che l'assegnazione sia stata generata e archiviata correttamente, vai alla console Google Cloud e seleziona Storage > "Cloud Storage". Seleziona il bucket aidemy-assignment che hai creato. Nel bucket dovresti vedere un file di testo denominato assignment-{random number}.txt. Fai clic sul file per scaricarlo e verificarne i contenuti. In questo modo viene verificato che un nuovo file contenga il nuovo compito appena generato.

👉 Nel terminale in cui è in esecuzione l'emulatore, digita ctrl+c per uscire. e chiudi il secondo terminale. 👉 Inoltre, nel terminale in cui è in esecuzione l'emulatore, esci dall'ambiente virtuale.
deactivate

👉 Successivamente, eseguiremo il deployment dell'agente di assegnazione nel 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
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud functions deploy assignment-agent \
--gen2 \
--timeout=540 \
--memory=2Gi \
--cpu=1 \
--set-env-vars="ASSIGNMENT_BUCKET=${ASSIGNMENT_BUCKET}" \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} \
--set-env-vars=OLLAMA_HOST=${OLLAMA_HOST} \
--region=us-central1 \
--runtime=python312 \
--source=. \
--entry-point=generate_assignment \
--trigger-topic=plan
Verifica il deployment andando alla console Google Cloud e poi a Cloud Run. Dovresti visualizzare un nuovo servizio denominato courses-agent. 
Ora che il flusso di lavoro di generazione dei compiti è stato implementato, testato e distribuito, possiamo passare alla fase successiva: rendere questi compiti accessibili all'interno del portale per gli studenti.
14. (FACOLTATIVO) Collaborazione basata sui ruoli con Gemini e DeepSeek - Continua
Generazione di siti web dinamici
Per migliorare il portale per gli studenti e renderlo più coinvolgente, implementeremo la generazione di HTML dinamico per le pagine dei compiti. L'obiettivo è aggiornare automaticamente il portale con un design nuovo e accattivante ogni volta che viene generato un nuovo compito. Sfrutta le funzionalità di programmazione del LLM per creare un'esperienza utente più dinamica e interessante.

👉 Nell'editor di Cloud Shell, modifica il file render.py all'interno della cartella portal, sostituisci
def render_assignment_page():
return ""
con il seguente snippet di codice:
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)}"
Utilizza il modello Gemini per generare dinamicamente l'HTML per il compito. Prende i contenuti dell'assegnazione come input e utilizza un prompt per chiedere a Gemini di creare una pagina HTML visivamente accattivante con uno stile creativo.
Successivamente, creeremo un endpoint che verrà attivato ogni volta che viene aggiunto un nuovo documento al bucket dell'attività:
👉 All'interno della cartella del portale, modifica il file app.py e SOSTITUISCI la riga ## REPLACE ME! RENDER ASSIGNMENT con il seguente codice:
@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
Quando viene attivata, recupera il nome del file e del bucket dai dati della richiesta, scarica i contenuti dell'assegnazione da Cloud Storage e chiama la funzione render_assignment_page per generare l'HTML.
👉 Eseguiamolo in locale:
cd ~/aidemy-bootstrap/portal
source env/bin/activate
python app.py
👉 Dal menu "Anteprima web" nella parte superiore della finestra di Cloud Shell, seleziona "Anteprima sulla porta 8080". L'applicazione si aprirà in una nuova scheda del browser. Vai al link Assegnazione nella barra di navigazione. A questo punto dovresti visualizzare una pagina vuota, il che è normale perché non abbiamo ancora stabilito il ponte di comunicazione tra l'agente di assegnazione e il portale per popolare dinamicamente i contenuti.

Interrompi lo script premendo Ctrl+C.
👉 Per incorporare queste modifiche ed eseguire il deployment del codice aggiornato, ricompila e invia l'immagine dell'agente del portale:
cd ~/aidemy-bootstrap/portal/
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
👉 Dopo aver eseguito il push della nuova immagine, esegui nuovamente il deployment del servizio Cloud Run. Esegui questo script per forzare l'aggiornamento di Cloud Run:
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud run services update aidemy-portal \
--region=us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
👉 Ora, implementeremo un trigger Eventarc che rimane in ascolto per rilevare qualsiasi nuovo oggetto creato (finalizzato) nel bucket di assegnazione. Questo trigger richiamerà automaticamente l'endpoint /render_assignment sul servizio del portale quando viene creato un nuovo file di compito.
gcloud config set project $(cat ~/project_id.txt)
export PROJECT_ID=$(gcloud config get project)
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$(gcloud storage service-agent --project $PROJECT_ID)" \
--role="roles/pubsub.publisher"
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud eventarc triggers create portal-assignment-trigger \
--location=us-central1 \
--service-account=$SERVICE_ACCOUNT_NAME \
--destination-run-service=aidemy-portal \
--destination-run-region=us-central1 \
--destination-run-path="/render_assignment" \
--event-filters="bucket=$ASSIGNMENT_BUCKET" \
--event-filters="type=google.cloud.storage.object.v1.finalized"
Per verificare che il trigger sia stato creato correttamente, vai alla pagina Trigger Eventarc nella console Google Cloud. Nella tabella dovresti vedere portal-assignment-trigger. Fai clic sul nome del trigger per visualizzarne i dettagli. 
Potrebbero essere necessari fino a 2-3 minuti perché il nuovo trigger diventi attivo.
Per vedere la generazione dell'assegnazione dinamica in azione, esegui questo comando per trovare l'URL dell'agente di pianificazione (se non lo hai a portata di mano):
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner
Trova l'URL del tuo agente del portale:
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal
Nell'agente di pianificazione, genera un nuovo piano didattico.

Dopo alcuni minuti (per consentire il completamento della generazione dell'audio, del compito e del rendering HTML), vai al portale per gli studenti.
👉 Fai clic sul link "Compito" nella barra di navigazione. Dovresti vedere un compito appena creato con un codice HTML generato dinamicamente. Ogni volta che viene generato un piano di insegnamento, deve essere un compito dinamico.

Congratulazioni per aver completato il sistema multi-agente Aidemy. Hai acquisito esperienza pratica e informazioni preziose su:
- I vantaggi dei sistemi multi-agente, tra cui modularità, scalabilità, specializzazione e manutenzione semplificata.
- L'importanza delle architetture basate su eventi per la creazione di applicazioni reattive e a basso accoppiamento.
- L'uso strategico degli LLM, abbinando il modello giusto all'attività e integrandoli con strumenti per un impatto reale.
- Pratiche di sviluppo cloud-native che utilizzano i servizi Google Cloud per creare soluzioni scalabili e affidabili.
- L'importanza di considerare la privacy dei dati e i modelli di self-hosting come alternativa alle soluzioni dei fornitori.
Ora hai una base solida per creare applicazioni sofisticate basate sull'AI su Google Cloud.
15. Sfide e passaggi successivi
Congratulazioni per aver creato il sistema multi-agente Aidemy. Hai creato una base solida per l'istruzione basata sull'AI. Ora, esaminiamo alcune sfide e potenziali miglioramenti futuri per espandere ulteriormente le sue funzionalità e soddisfare le esigenze del mondo reale:
Apprendimento interattivo con domande e risposte live:
- Sfida: riesci a sfruttare l'API Live di Gemini 2 per creare una funzionalità di domande e risposte in tempo reale per gli studenti? Immagina una classe virtuale in cui gli studenti possono porre domande e ricevere risposte immediate basate sull'AI.
Invio e valutazione automatici dei compiti:
- Sfida: progettare e implementare un sistema che consenta agli studenti di inviare i compiti in formato digitale e di farli valutare automaticamente dall'AI, con un meccanismo per rilevare e prevenire il plagio. Questa sfida offre una grande opportunità per esplorare la Retrieval-Augmented Generation (RAG) per migliorare l'accuratezza e l'affidabilità dei processi di valutazione e rilevamento del plagio.

16. Esegui la pulizia
Ora che abbiamo creato ed esplorato il nostro sistema multi-agente Aidemy, è il momento di pulire il nostro ambiente Google Cloud.
👉 Elimina i servizi 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
👉 Elimina trigger 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
👉 Elimina l'argomento Pub/Sub
gcloud pubsub topics delete plan --project="$PROJECT_ID" --quiet
👉 Elimina l'istanza Cloud SQL
gcloud sql instances delete aidemy --quiet
👉 Elimina il repository Artifact Registry
gcloud artifacts repositories delete agent-repository --location=us-central1 --quiet
👉 Elimina i secret di Secret Manager
gcloud secrets delete db-user --quiet
gcloud secrets delete db-pass --quiet
gcloud secrets delete db-name --quiet
👉 Elimina l'istanza Compute Engine (se creata per Deepseek)
gcloud compute instances delete ollama-instance --zone=us-central1-a --quiet
👉 Elimina la regola firewall per l'istanza Deepseek
gcloud compute firewall-rules delete allow-ollama-11434 --quiet
👉 Elimina i bucket Cloud Storage
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export ASSIGNMENT_BUCKET=$(gcloud storage buckets list --format="value(name)" | grep aidemy-assignment)
gsutil rm -r gs://$COURSE_BUCKET_NAME
gsutil rm -r gs://$ASSIGNMENT_BUCKET
