Aidemy: como criar sistemas multiagentes com LangGraph, EDA e IA generativa no Google Cloud

1. Introdução

Olá! Então, você gosta da ideia de agentes, pequenos ajudantes que podem fazer as coisas por você sem que você precise mover um dedo, certo? Incrível! Mas, vamos ser sinceros, um agente nem sempre é suficiente, principalmente quando você está trabalhando em projetos maiores e mais complexos. Provavelmente, você vai precisar de uma equipe inteira! É aí que entram os sistemas multiagentes.

Os agentes, quando alimentados por LLMs, oferecem uma flexibilidade incrível em comparação com a programação fixa tradicional. Mas, e sempre há um "mas", eles vêm com um conjunto próprio de desafios complicados. É exatamente isso que vamos abordar neste workshop.

título

Confira o que você vai aprender, como se fosse uma evolução do seu jogo de agente:

Como criar seu primeiro agente com o LangGraph: vamos colocar a mão na massa e criar seu próprio agente usando o LangGraph, um framework conhecido. Você vai aprender a criar ferramentas que se conectam a bancos de dados, usar a API Gemini 2 mais recente para fazer pesquisas na Internet e otimizar os comandos e as respostas para que seu agente possa interagir não apenas com LLMs, mas também com serviços atuais. Também vamos mostrar como funciona a chamada de função.

Orquestração de agentes do seu jeito: vamos explorar diferentes maneiras de orquestrar seus agentes, desde caminhos simples e diretos até cenários mais complexos com vários caminhos. É como direcionar o fluxo da sua equipe de agentes.

Sistemas multiagentes: você vai descobrir como configurar um sistema em que seus agentes podem colaborar e realizar tarefas juntos, tudo graças a uma arquitetura orientada a eventos.

Liberdade de LLM: use o melhor para o trabalho. Não estamos presos a apenas um LLM. Você vai aprender a usar vários LLMs, atribuindo a eles funções diferentes para aumentar a capacidade de resolução de problemas usando "modelos de pensamento" legais.

Conteúdo dinâmico? Sem problemas!: Imagine seu agente criando conteúdo dinâmico personalizado para cada usuário em tempo real. Vamos mostrar como fazer isso.

Levando para a nuvem com o Google Cloud: esqueça apenas brincar em um notebook. Vamos mostrar como arquitetar e implantar seu sistema multiagente no Google Cloud para que ele esteja pronto para o mundo real.

Esse projeto será um bom exemplo de como usar todas as técnicas que abordamos.

2. Arquitetura

Ser professor ou trabalhar na área da educação pode ser muito gratificante, mas vamos admitir que a carga de trabalho, principalmente a preparação, pode ser desafiadora. Além disso, muitas vezes não há funcionários suficientes, e as aulas particulares podem ser caras. Por isso, estamos propondo um assistente de ensino com tecnologia de IA. Essa ferramenta pode aliviar a carga dos educadores e ajudar a diminuir a lacuna causada pela falta de pessoal e de tutoria acessível.

Nosso assistente de ensino de IA pode criar planos de aula detalhados, testes divertidos, resumos em áudio fáceis de acompanhar e atividades personalizadas. Assim, os professores podem se concentrar no que fazem de melhor: se conectar com os estudantes e ajudá-los a se apaixonar pelo aprendizado.

O sistema tem dois sites: um para professores criarem planos de aula para as próximas semanas,

Planejador

e um para os estudantes acessarem testes, resumos em áudio e atividades. Portal

Vamos analisar a arquitetura que alimenta nosso assistente de ensino, o Aidemy. Como você pode ver, dividimos em vários componentes principais, todos trabalhando juntos para que isso aconteça.

Arquitetura

Principais elementos e tecnologias de arquitetura:

Google Cloud Platform (GCP): central para todo o sistema.

  • Vertex AI: acessa os LLMs Gemini do Google.
  • Cloud Run: plataforma sem servidor para implantação de agentes e funções conteinerizados.
  • Cloud SQL: banco de dados PostgreSQL para dados do currículo.
  • Pub/Sub e Eventarc: base da arquitetura orientada a eventos, permitindo a comunicação assíncrona entre componentes.
  • Cloud Storage: armazena resumos de áudio e arquivos de atividades.
  • Secret Manager: gerencia credenciais de banco de dados com segurança.
  • Artifact Registry: armazena imagens do Docker para os agentes.
  • Compute Engine: para implantar um LLM autohospedado em vez de usar soluções de fornecedores

LLMs: o "cérebro" do sistema.

  • Modelos do Gemini do Google: (Gemini x Pro, Gemini x Flash, Gemini x Flash Thinking) usados para planejamento de aulas, geração de conteúdo, criação dinâmica de HTML, explicação de testes e combinação de atividades.
  • DeepSeek: usado para a tarefa especializada de gerar atividades de autoestudo

LangChain e LangGraph: frameworks para desenvolvimento de aplicativos com LLM

  • Facilita a criação de fluxos de trabalho complexos com vários agentes.
  • Permite a organização inteligente de ferramentas (chamadas de API, consultas de banco de dados, pesquisas na Web).
  • Implementa a arquitetura orientada a eventos para escalonabilidade e flexibilidade do sistema.

Em essência, nossa arquitetura combina o poder dos LLMs com dados estruturados e comunicação orientada a eventos, tudo executado no Google Cloud. Isso nos permite criar um assistente de ensino escalonável, confiável e eficaz.

3. Antes de começar

No console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud. Verifique se o faturamento está ativado para seu projeto do Cloud. Saiba como verificar se o faturamento está ativado em um projeto.

Ativar o Gemini Code Assist no Cloud Shell IDE

👉 No console do Google Cloud, acesse as ferramentas do Gemini Code Assist e ative o Gemini Code Assist sem custos financeiros ao concordar com os termos e condições.

01-04-code-assist-enable.png

Ignore a configuração de permissão e saia desta página.

Trabalhar no editor do Cloud Shell

👉Clique em Ativar Cloud Shell na parte de cima do console do Google Cloud (é o ícone em forma de terminal na parte de cima do painel do Cloud Shell) e clique no botão "Abrir Editor" (parece uma pasta aberta com um lápis). Isso vai abrir o editor de código do Cloud Shell na janela. Um explorador de arquivos vai aparecer no lado esquerdo.

Cloud Shell

👉Clique no botão Fazer login no Cloud Code na barra de status inferior, conforme mostrado. Autorize o plug-in conforme instruído. Se a barra de status mostrar Cloud Code – sem projeto, clique na opção e escolha o projeto do Google Cloud com que você quer trabalhar no menu suspenso "Selecionar um projeto do Google Cloud".

Projeto de login

👉Abra o terminal no IDE na nuvem, Novo terminal ou Novo terminal

👉No terminal, verifique se você já está autenticado e se o projeto está definido como seu ID do projeto usando o seguinte comando:

gcloud auth list

👉Execute e substitua <YOUR_PROJECT_ID> pelo ID do seu projeto:

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

👉Execute o comando a seguir para ativar as APIs do Google Cloud necessárias:

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

Isso pode levar alguns minutos.

Configurar permissão

👉Configure a permissão da conta de serviço. No terminal, execute :

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"

👉 Conceda permissões. No terminal, execute :

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

👉Valide o resultado no console do IAMConsole do IAM

👉Execute os comandos a seguir no terminal para criar uma instância do Cloud SQL chamada aidemy. Vamos precisar disso mais tarde, mas como esse processo pode levar algum tempo, vamos fazer isso agora.

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. Como criar o primeiro agente

Antes de abordarmos sistemas multiagentes complexos, precisamos estabelecer um bloco de construção fundamental: um agente único e funcional. Nesta seção, vamos dar os primeiros passos criando um agente simples de "provedor de livros". O agente de provedor de livros recebe uma categoria como entrada e usa um LLM do Gemini para gerar um livro de representação JSON nessa categoria. Em seguida, ele veicula essas recomendações de livros como um endpoint de API REST .

Provedor de livros

👉Em outra guia do navegador, abra o Console do Google Cloud. No menu de navegação (☰), acesse "Cloud Run". Clique no botão "+ ... ESCREVER UMA FUNÇÃO".

Criar função

👉Em seguida, vamos configurar as definições básicas da função do Cloud Run:

  • Nome do serviço: book-provider
  • Região: us-central1
  • Ambiente de execução: Python 3.12
  • Autenticação: Allow unauthenticated invocations para "Ativado".

👉Deixe as outras configurações como padrão e clique em Criar. Isso vai levar você ao editor de código-fonte.

Você vai encontrar arquivos main.py e requirements.txt pré-preenchidos.

O main.py vai conter a lógica de negócios da função, e o requirements.txt vai conter os pacotes necessários.

👉Agora podemos escrever um pouco de código. Mas, antes de começar, vamos ver se o Gemini Code Assist pode nos ajudar. Volte ao editor do Cloud Shell e clique no ícone do Gemini Code Assist na parte de cima para abrir o chat.

Gemini Code Assist

👉 Cole o seguinte pedido na caixa de comando:

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

Em seguida, o Code Assist vai gerar uma possível solução, fornecendo o código-fonte e um arquivo de dependência requirements.txt. (NÃO USE ESTE CÓDIGO)

Recomendamos que você compare o código gerado pelo Code Assist com a solução testada e correta fornecida abaixo. Isso permite avaliar a eficácia da ferramenta e identificar possíveis discrepâncias. Embora os LLMs nunca devam ser confiáveis sem verificação, o Code Assist pode ser uma ótima ferramenta para prototipagem rápida e geração de estruturas de código iniciais, sendo útil para um bom começo.

Como este é um workshop, vamos continuar com o código verificado fornecido abaixo. No entanto, fique à vontade para testar o código gerado pelo Code Assist quando quiser e entender melhor os recursos e as limitações dele.

👉Volte ao editor de código-fonte da função do Cloud Run (na outra guia do navegador). Substitua cuidadosamente o conteúdo atual de main.py pelo código abaixo:

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)

👉Substitua o conteúdo de requirements.txt pelo seguinte:

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

👉Vamos definir o ponto de entrada da função: recommended

03-02-function-create.png

👉Clique em SALVAR E IMPLANTAR (ou SALVAR E REIMPLANTAR) para implantar a função. Aguarde a conclusão do upgrade. O Console do Cloud vai mostrar o status. Isso pode levar alguns minutos.

texto alternativo 👉Depois da implantação, volte ao editor do Cloud Shell e execute o seguinte comando no terminal:

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

Ele vai mostrar alguns dados de livros no 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"}
]

Parabéns! Você implantou uma função do Cloud Run. Esse é um dos serviços que vamos integrar ao desenvolver nosso agente da Aidemy.

5. Ferramentas de criação: como conectar agentes a serviços RESTFUL e dados

Faça o download do projeto Bootstrap Skeleton. Verifique se você está no editor do Cloud Shell. No terminal, execute

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

Depois de executar esse comando, uma nova pasta chamada aidemy-bootstrap será criada no seu ambiente do Cloud Shell.

No painel "Explorador" do editor do Cloud Shell (geralmente à esquerda), agora você vai encontrar a pasta criada quando clonou o repositório Git aidemy-bootstrap. Abra a pasta raiz do projeto no Explorer. Você vai encontrar uma subpasta planner. Abra ela também. Explorador de projetos

Vamos começar a criar as ferramentas que nossos agentes vão usar para serem realmente úteis. Como você sabe, os LLMs são excelentes em raciocínio e geração de texto, mas precisam de acesso a recursos externos para realizar tarefas do mundo real e fornecer informações precisas e atualizadas. Pense nessas ferramentas como o "canivete suíço" do agente, a ele a capacidade de interagir com o mundo.

Ao criar um agente, é fácil codificar muitos detalhes. Isso cria um agente que não é flexível. Em vez disso, ao criar e usar ferramentas, o agente tem acesso a lógica ou sistemas externos, o que oferece os benefícios do LLM e da programação tradicional.

Nesta seção, vamos criar a base do agente planejador, que os professores vão usar para gerar planos de aula. Antes de o agente começar a gerar um plano, queremos definir limites fornecendo mais detalhes sobre o assunto e o tema. Vamos criar três ferramentas:

  1. Chamada de API RESTful:interação com uma API preexistente para recuperar dados.
  2. Consulta de banco de dados:busca dados estruturados de um banco de dados do Cloud SQL.
  3. Pesquisa Google:acesso a informações em tempo real da Web.

Como buscar recomendações de livros de uma API

Primeiro, vamos criar uma ferramenta que recupera recomendações de livros da API book-provider implantada na seção anterior. Isso demonstra como um agente pode aproveitar os serviços atuais.

Recomendar livro

No Editor do Cloud Shell, abra o projeto aidemy-bootstrap que você clonou na seção anterior.

👉Edite o book.py na pasta planner, e cole o seguinte código no final do arquivo:

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

Explicação:

  • recommend_book(query: str): essa função usa a consulta de um usuário como entrada.
  • Interação com LLM: usa o LLM para extrair a categoria da consulta. Isso demonstra como usar o LLM para criar parâmetros para ferramentas.
  • Chamada de API: faz uma solicitação POST para a API do provedor de livros, transmitindo a categoria e o número desejado de livros.

👉Para testar essa nova função, defina a variável de ambiente e execute :

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

👉Instale as dependências e execute o código para garantir que ele funcione. Execute:

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

Uma string JSON com recomendações de livros recuperadas da API book-provider vai aparecer. Os resultados são gerados aleatoriamente. Seus livros podem não ser os mesmos, mas você vai receber duas recomendações de livros no 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 você vir isso, a primeira ferramenta está funcionando corretamente.

Em vez de criar explicitamente uma chamada de API RESTful com parâmetros específicos, estamos usando linguagem natural ("Estou fazendo um curso..."). Em seguida, o agente extrai de forma inteligente os parâmetros necessários (como a categoria) usando o NLP, destacando como ele aproveita a compreensão de linguagem natural para interagir com a API.

comparar chamada

👉Remova o seguinte código de teste do 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."))

Como extrair dados de currículo de um banco de dados

Em seguida, vamos criar uma ferramenta que busca dados estruturados de currículo em um banco de dados PostgreSQL do Cloud SQL. Isso permite que o agente acesse uma fonte confiável de informações para o planejamento de aulas.

criar db

Lembra da instância do Cloud SQL aidemy que você criou na etapa anterior? É aqui que ele será usado.

👉 No terminal, execute o comando a seguir para criar um banco de dados chamado aidemy-db na nova instância.

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

Vamos verificar a instância no Cloud SQL no console do Google Cloud. Uma instância do Cloud SQL chamada aidemy vai aparecer na lista.

👉 Clique no nome da instância para ver os detalhes. 👉 Na página de detalhes da instância do Cloud SQL, clique em Cloud SQL Studio no menu de navegação à esquerda. Uma nova guia será aberta.

Selecione aidemy-db como o banco de dados, insira postgres como usuário e 1234qwer como a senha.

Clique em Autenticar.

login no sql studio

👉No editor de consultas do SQL Studio, acesse a guia Editor 1 e cole o seguinte código 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.');

Esse código SQL cria uma tabela chamada curriculums e insere alguns dados de exemplo.

👉 Clique em Executar para executar o código SQL. Uma mensagem de confirmação indica que as instruções foram executadas.

👉 Expanda o explorador, encontre a tabela recém-criada curriculums e clique em consulta. Uma nova guia do editor será aberta com o SQL gerado para você.

sql studio select table

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

👉Clique em Executar.

A tabela de resultados vai mostrar as linhas de dados inseridas na etapa anterior, confirmando que a tabela e os dados foram criados corretamente.

Agora que você criou um banco de dados com dados de currículo de amostra, vamos criar uma ferramenta para recuperá-los.

👉No editor do Cloud Code, edite o arquivo curriculums.py na pasta aidemy-bootstrap e cole o seguinte código no final do arquivo:

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

Explicação:

  • Variáveis de ambiente: o código recupera credenciais de banco de dados e informações de conexão de variáveis de ambiente (mais detalhes abaixo).
  • connect_with_connector(): essa função usa o conector do Cloud SQL para estabelecer uma conexão segura com o banco de dados.
  • get_curriculum(year: int, subject: str): essa função recebe o ano e a disciplina como entrada, consulta a tabela de currículos e retorna a descrição correspondente.

👉Antes de executar o código, precisamos definir algumas variáveis de ambiente. No terminal, execute:

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"

👉Para testar, adicione o seguinte código ao final de curriculums.py:

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

👉Execute o código:

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

A descrição do currículo de matemática do 6º ano será impressa no console.

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

Se a descrição do currículo aparecer, a ferramenta de banco de dados está funcionando corretamente. Interrompa o script pressionando Ctrl+C se ele ainda estiver em execução.

👉Remova o seguinte código de teste do curriculums.py

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

👉Saia do ambiente virtual. No terminal, execute:

deactivate

6. Ferramentas de criação: acessar informações em tempo real da Web

Por fim, vamos criar uma ferramenta que usa a integração do Gemini 2 e da Pesquisa Google para acessar informações em tempo real da Web. Isso ajuda o agente a ficar atualizado e fornecer resultados relevantes.

A integração do Gemini 2 com a API da Pesquisa Google melhora os recursos do agente, oferecendo resultados de pesquisa mais precisos e contextualmente relevantes. Isso permite que os agentes acessem informações atualizadas e embasem as respostas em dados do mundo real, minimizando as alucinações. A integração aprimorada da API também facilita consultas em linguagem natural, permitindo que os agentes formulem solicitações de pesquisa complexas e sutis.

Pesquisar

Essa função usa uma consulta de pesquisa, um currículo, uma disciplina e um ano como entrada e usa a API Gemini e a ferramenta de Pesquisa Google para recuperar informações relevantes da Internet. Se você olhar de perto, ele está usando o SDK de IA generativa do Google para fazer chamadas de função sem usar nenhum outro framework.

👉Edite search.py na pasta aidemy-bootstrap e cole o seguinte código no final do arquivo:

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)

Explicação:

  • Definindo a ferramenta: google_search_tool: encapsulando o objeto GoogleSearch em uma ferramenta
  • search_latest_resource(search_text: str, subject: str, year: int): essa função usa uma consulta de pesquisa, um assunto e um ano como entrada e usa a API Gemini para realizar uma pesquisa no Google.
  • GenerateContentConfig: define que ele tem acesso à ferramenta GoogleSearch.

O modelo Gemini analisa internamente o search_text e determina se pode responder à pergunta diretamente ou se precisa usar a ferramenta GoogleSearch. Essa é uma etapa essencial que acontece no processo de raciocínio do LLM. O modelo foi treinado para reconhecer situações em que ferramentas externas são necessárias. Se o modelo decidir usar a ferramenta GoogleSearch, o SDK de IA generativa do Google vai processar a invocação. O SDK usa a decisão do modelo e os parâmetros gerados e os envia para a API Google Search. Essa parte fica oculta do usuário no código.

Em seguida, o modelo do Gemini integra os resultados da pesquisa à resposta. Ele pode usar as informações para responder à pergunta do usuário, gerar um resumo ou realizar alguma outra tarefa.

👉Para testar, execute o código:

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

Você vai encontrar a resposta da API Gemini Search com resultados relacionados a "Programa de matemática do 5º ano". A saída exata vai depender dos resultados da pesquisa, mas será um objeto JSON com informações sobre a pesquisa.

Se você encontrar resultados da pesquisa, a ferramenta está funcionando corretamente. Interrompa o script pressionando Ctrl+C se ele ainda estiver em execução.

👉E remova a última parte do código.

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)

👉Saia do ambiente virtual. No terminal, execute:

deactivate

Parabéns! Agora você criou três ferramentas poderosas para seu agente de planejamento: um conector de API, um conector de banco de dados e uma ferramenta da Pesquisa Google. Com essas ferramentas, o agente pode acessar as informações e os recursos necessários para criar planos de ensino eficazes.

7. Orquestração com o LangGraph

Agora que criamos nossas ferramentas individuais, é hora de orquestrá-las usando o LangGraph. Isso vai permitir que criemos um agente "planner" mais sofisticado que pode decidir de forma inteligente quais ferramentas usar e quando, com base na solicitação do usuário.

O LangGraph é uma biblioteca Python criada para facilitar a criação de aplicativos com estado e vários atores usando modelos de linguagem grandes (LLMs). Pense nisso como um framework para orquestrar conversas e fluxos de trabalho complexos envolvendo LLMs, ferramentas e outros agentes.

Principais conceitos:

  • Estrutura do gráfico:o LangGraph representa a lógica do seu aplicativo como um gráfico direcionado. Cada no gráfico representa uma etapa do processo (por exemplo, uma chamada para um LLM, uma invocação de ferramenta, uma verificação condicional). As arestas definem o fluxo de execução entre os nós.
  • Estado:o LangGraph gerencia o estado do seu aplicativo à medida que ele se move pelo gráfico. Esse estado pode incluir variáveis como a entrada do usuário, os resultados de chamadas de função, saídas intermediárias de LLMs e qualquer outra informação que precise ser preservada entre as etapas.
  • Nós:cada nó representa um cálculo ou uma interação. Elas podem ser:
    • Nós de ferramentas:use uma ferramenta (por exemplo, faça uma pesquisa na Web, consulte um banco de dados).
    • Nós de função:executam uma função Python.
  • Arestas:conectam nós, definindo o fluxo de execução. Elas podem ser:
    • Arestas diretas:um fluxo simples e incondicional de um nó para outro.
    • Arestas condicionais:o fluxo depende do resultado de um nó condicional.

LangGraph

Vamos usar o LangGraph para implementar a orquestração. Vamos editar o arquivo aidemy.py na pasta aidemy-bootstrap para definir nossa lógica do LangGraph.

👉 Adicione o seguinte código ao final de

aidemy.py:

tools = [get_curriculum, search_latest_resource, recommend_book]

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

                        """
                    )
                )

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

Essa função é responsável por pegar o estado atual da conversa, fornecer uma mensagem do sistema ao LLM e pedir que ele gere uma resposta. O LLM pode responder diretamente ao usuário ou usar uma das ferramentas disponíveis.

tools : esta lista representa o conjunto de ferramentas disponíveis para o agente. Ele contém três funções de ferramenta que definimos nas etapas anteriores: get_curriculum, search_latest_resource e recommend_book. llm.bind_tools(tools): "vincula" a lista de ferramentas ao objeto llm. Ao vincular as ferramentas, você informa ao LLM que elas estão disponíveis e fornece informações sobre como usá-las (por exemplo, os nomes das ferramentas, os parâmetros que elas aceitam e o que fazem).

Vamos usar o LangGraph para implementar a orquestração.

👉 Adicione o seguinte código ao final de

aidemy.py:

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

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

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


    return teaching_plan_result

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

Explicação:

  • StateGraph(MessagesState):cria um objeto StateGraph. Um StateGraph é um conceito fundamental no LangGraph. Ele representa o fluxo de trabalho do seu agente como um gráfico, em que cada nó representa uma etapa do processo. Pense nisso como definir o projeto de como o agente vai raciocinar e agir.
  • Borda condicional:originado do nó "determine_tool", o argumento tools_condition provavelmente é uma função que determina qual borda seguir com base na saída da função determine_tool. As arestas condicionais permitem que o gráfico ramifique com base na decisão do LLM sobre qual ferramenta usar (ou se deve responder diretamente ao usuário). É aqui que entra em jogo a "inteligência" do agente: ele pode adaptar dinamicamente o comportamento com base na situação.
  • Loop:adiciona uma aresta ao gráfico que conecta o nó "tools" ao nó "determine_tool". Isso cria um loop no gráfico, permitindo que o agente use ferramentas repetidamente até coletar informações suficientes para concluir a tarefa e dar uma resposta satisfatória. Esse loop é crucial para tarefas complexas que exigem várias etapas de raciocínio e coleta de informações.

Agora, vamos testar nosso agente de planejamento para ver como ele organiza as diferentes ferramentas.

Esse código vai executar a função "prep_class" com uma entrada específica do usuário, simulando uma solicitação para criar um plano de ensino de matemática do 5º ano em geometria, usando o currículo, recomendações de livros e os recursos mais recentes da Internet.

👉 No terminal, se você o fechou ou as variáveis de ambiente não estão mais definidas, execute novamente os comandos a seguir.

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"

👉Execute o código:

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

Acompanhe o registro no terminal. Você vai notar que o agente está chamando todas as três ferramentas (recebendo o currículo escolar, recebendo recomendações de livros e pesquisando os recursos mais recentes) antes de fornecer o plano de ensino final. Isso demonstra que a orquestração do LangGraph está funcionando corretamente e que o agente está usando de forma inteligente todas as ferramentas disponíveis para atender à solicitação do usuário.

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

Para interromper o script, pressione Ctrl+C se ele ainda estiver em execução.

👉 (ESTA ETAPA É OPCIONAL) substitua o código de teste por um comando diferente, que exige a chamada de ferramentas diferentes.

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 você fechou o terminal ou as variáveis de ambiente não estão mais definidas, execute novamente os seguintes comandos:

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"

👉 (ESTA ETAPA É OPCIONAL. FAÇA ISSO APENAS SE VOCÊ EXECUTOU A ETAPA ANTERIOR) Execute o código novamente:

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

O que você notou desta vez? Quais ferramentas o agente chamou? Você vai notar que o agente só chama a ferramenta search_latest_resource desta vez. Isso acontece porque o comando não especifica que precisa das outras duas ferramentas, e nosso LLM é inteligente o suficiente para não chamar as outras ferramentas.

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

Para interromper o script, pressione Ctrl+C.

👉 (NÃO PULE ESTA ETAPA!) Remova o código de teste para manter o arquivo aidemy.py limpo :

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

Agora que definimos a lógica do agente, vamos iniciar o web app Flask. Isso vai fornecer uma interface familiar baseada em formulários para os professores interagirem com o agente. Embora as interações com chatbots sejam comuns com LLMs, estamos optando por uma interface tradicional de envio de formulários, já que ela pode ser mais intuitiva para muitos educadores.

👉 Se você fechou o terminal ou as variáveis de ambiente não estão mais definidas, execute novamente os seguintes comandos:

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"

👉 Agora, inicie a interface da Web.

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

Procure mensagens de inicialização na saída do terminal do Cloud Shell. O Flask geralmente imprime mensagens indicando que está em execução e em qual 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.

👉 No menu "Visualização na Web" no canto superior direito, escolha Visualizar na porta 8080. O Cloud Shell vai abrir uma nova guia ou janela do navegador com a prévia da Web do seu aplicativo.

Página da Web

Na interface do aplicativo, selecione 5 para "Ano", o assunto Mathematics e digite Geometry na solicitação de complemento.

👉 Se você saiu da interface do aplicativo, volte para ela. A saída gerada vai aparecer.

👉 No terminal, pressione Ctrl+C para interromper o script.

👉 No terminal, saia do ambiente virtual:

deactivate

8. Como implantar o agente de planejamento na nuvem

Criar e enviar imagem para o registro

Visão geral

Agora é hora de implantar isso na nuvem.

👉 No terminal, crie um repositório de artefatos para armazenar a imagem do Docker que vamos criar.

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

Você vai ver "Created repository [agent-repository]".

👉 Execute o comando a seguir para criar a imagem do 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 .

👉 Precisamos adicionar uma nova tag à imagem para que ela seja hospedada no Artifact Registry em vez do GCR e enviar a imagem marcada para o 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

Depois que o envio for concluído, verifique se a imagem foi armazenada com sucesso no Artifact Registry.

👉 Acesse o Artifact Registry no console do Google Cloud. Encontre a imagem aidemy-planner no repositório agent-repository. Imagem do planejador da Aidemy

Como proteger credenciais de banco de dados com o Secret Manager

Para gerenciar e acessar credenciais de banco de dados com segurança, vamos usar o Secret Manager do Google Cloud. Isso evita a codificação de informações sensíveis no código do aplicativo e aumenta a segurança.

Vamos criar secrets individuais para o nome de usuário, a senha e o nome do banco de dados. Essa abordagem permite gerenciar cada credencial de forma independente.

👉 No terminal, execute o seguinte:

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

Usar o Secret Manager é uma etapa importante para proteger seu aplicativo e evitar a exposição acidental de credenciais sensíveis. Ele segue as práticas recomendadas de segurança para implantações na nuvem.

Implantar no Cloud Run

O Cloud Run é uma plataforma sem servidor totalmente gerenciada que permite implantar aplicativos conteinerizados com rapidez e facilidade. Ele abstrai o gerenciamento da infraestrutura, permitindo que você se concentre na criação e implantação do código. Vamos implantar nosso planejador como um serviço do Cloud Run.

👉No console do Google Cloud, acesse Cloud Run. Clique em IMPLANTAR CONTÊINER e selecione SERVICE. Configure o serviço do Cloud Run:

Cloud Run

  1. Imagem do contêiner: clique em "Selecionar" no campo de URL. Encontre o URL da imagem que você enviou para o Artifact Registry (por exemplo, us-central1-docker.pkg.dev/YOUR_PROJECT_ID/agent-repository/aidemy-planner/YOUR_IMG).
  2. Nome do serviço: aidemy-planner
  3. Região: selecione a região us-central1.
  4. Autenticação: para fins deste workshop, você pode permitir "Permitir invocações não autenticadas". Para produção, é recomendável restringir o acesso.
  5. Abra a seção "Contêineres, volumes, rede, segurança" e defina o seguinte na guia Contêineres:
    • Guia "Configurações":
      • Recursos
        • memory : 2GiB
    • Guia "Variáveis e secrets":
      • Variáveis de ambiente: clique no botão + Adicionar variável para adicionar as seguintes variáveis:
        • Adicione nome: GOOGLE_CLOUD_PROJECT e valor: <YOUR_PROJECT_ID>
        • Adicione o nome: BOOK_PROVIDER_URL e defina o valor como o URL da função do provedor de livros, que pode ser determinado usando o seguinte comando no terminal:
          gcloud config set project $(cat ~/project_id.txt)
          gcloud run services describe book-provider \
              --region=us-central1 \
              --project=$PROJECT_ID \
              --format="value(status.url)"
          
      • Na seção Secrets expostos como variáveis de ambiente, adicione os seguintes secrets clicando no botão + Referência como um secret:
        • Adicione nome: DB_USER, secret: selecione db-user e versão:latest
        • Adicione nome: DB_PASS, secret: selecione db-pass e versão:latest
        • Adicione nome: DB_NAME, secret: selecione db-name e versão:latest

Definir secret

Mantenha os outros valores como padrão.

👉 Clique em CRIAR.

O Cloud Run vai implantar seu serviço.

Depois da implantação, se você ainda não estiver na página de detalhes, clique no nome do serviço para acessar essa página. O URL implantado aparece na parte de cima.

URL

👉 Na interface do aplicativo, selecione 7 para o ano, escolha Mathematics como assunto e insira Algebra no campo "Solicitação de complemento".

👉 Clique em Gerar plano. Isso vai fornecer ao agente o contexto necessário para gerar um plano de aula personalizado.

Parabéns! Você criou um plano de ensino usando nosso poderoso agente de IA. Isso demonstra o potencial dos agentes para reduzir significativamente a carga de trabalho e simplificar as tarefas, melhorando a eficiência e facilitando a vida dos educadores.

9. Sistemas multiagentes

Agora que implementamos a ferramenta de criação de planos de ensino, vamos focar na criação do portal do estudante. Nesse portal, os estudantes têm acesso a testes, resumos em áudio e atividades relacionadas ao curso. Devido ao escopo dessa funcionalidade, vamos aproveitar o poder dos sistemas multiagentes para criar uma solução modular e escalonável.

Como discutimos antes, em vez de depender de um único agente para lidar com tudo, um sistema multiagente permite dividir a carga de trabalho em tarefas menores e especializadas, cada uma processada por um agente dedicado. Essa abordagem oferece várias vantagens importantes:

Modularidade e capacidade de manutenção: em vez de criar um único agente que faz tudo, crie agentes menores e especializados com responsabilidades bem definidas. Essa modularidade facilita a compreensão, a manutenção e a depuração do sistema. Quando um problema surge, é possível isolá-lo em um agente específico, em vez de ter que analisar uma base de código enorme.

Escalonabilidade: o escalonamento de um único agente complexo pode ser um gargalo. Com um sistema multiagente, é possível escalonar agentes individuais com base nas necessidades específicas deles. Por exemplo, se um agente estiver processando um grande volume de solicitações, você poderá ativar mais instâncias dele sem afetar o restante do sistema.

Especialização da equipe: pense assim: você não pediria para um engenheiro criar um aplicativo inteiro do zero. Em vez disso, você monta uma equipe de especialistas, cada um com experiência em uma área específica. Da mesma forma, um sistema multiagente permite aproveitar os pontos fortes de diferentes LLMs e ferramentas, atribuindo-os a agentes mais adequados para tarefas específicas.

Desenvolvimento paralelo: equipes diferentes podem trabalhar em agentes diferentes simultaneamente, acelerando o processo de desenvolvimento. Como os agentes são independentes, as mudanças em um deles têm menos probabilidade de afetar os outros.

Arquitetura orientada a eventos

Para permitir uma comunicação e coordenação eficazes entre esses agentes, vamos usar uma arquitetura orientada a eventos. Isso significa que os agentes vão reagir a "eventos" que acontecem no sistema.

Os agentes se inscrevem em tipos de eventos específicos (por exemplo, "plano de ensino gerado", "atividade criada"). Quando um evento ocorre, os agentes relevantes são notificados e podem reagir de acordo. Esse desacoplamento promove flexibilidade, escalonabilidade e capacidade de resposta em tempo real.

Visão geral

Agora, para começar, precisamos de uma maneira de transmitir esses eventos. Para fazer isso, vamos configurar um tópico do Pub/Sub. Vamos começar criando um tópico chamado plan.

👉 Acesse o Pub/Sub do Google Cloud Console.

👉 Clique no botão Criar tópico.

👉 Configure o Tópico com o ID/nome plan e desmarqueAdd a default subscription. Deixe o restante como padrão e clique em Criar.

A página do Pub/Sub será atualizada, e o tópico recém-criado vai aparecer na tabela. Criar tópico

Agora, vamos integrar a funcionalidade de publicação de eventos do Pub/Sub ao nosso agente de planejamento. Vamos adicionar uma nova ferramenta que envia um evento "plan" ao tópico do Pub/Sub que acabamos de criar. Esse evento vai sinalizar para outros agentes no sistema (como os do portal do estudante) que um novo plano de ensino está disponível.

👉Volte ao editor do Cloud Code e abra o arquivo app.py localizado na pasta planner. Vamos adicionar uma função que publica o evento. Substituir:

##ADD SEND PLAN EVENT FUNCTION HERE

pelo código a seguir

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: essa função usa o plano de ensino gerado como entrada, cria um cliente editor do Pub/Sub, constrói o caminho do tópico, converte o plano de ensino em uma string JSON e publica a mensagem no tópico.

No mesmo arquivo app.py

👉Atualize o comando para instruir o agente a enviar o evento do plano de ensino para o tópico do Pub/Sub depois de gerar o plano. *Substituir

### ADD send_plan_event CALL

com o seguinte:

send_plan_event(teaching_plan)

Ao adicionar a ferramenta send_plan_event e modificar o comando, permitimos que nosso agente de planejamento publique eventos no Pub/Sub, permitindo que outros componentes do nosso sistema reajam à criação de novos planos de ensino. Agora, teremos um sistema multiagente funcional nas seções a seguir.

10. Capacitação dos estudantes com testes sob demanda

Imagine um ambiente de aprendizado em que os estudantes têm acesso a uma quantidade infinita de testes personalizados para os planos de aprendizado específicos deles. Esses testes oferecem feedback imediato, incluindo respostas e explicações, promovendo uma compreensão mais profunda do material. Esse é o potencial que queremos liberar com nosso portal de testes com tecnologia de IA.

Para dar vida a essa visão, vamos criar um componente de geração de testes que pode criar perguntas de múltipla escolha com base no conteúdo do plano de ensino.

Visão geral

👉 No painel do Explorer do editor do Cloud Code, navegue até a pasta portal. Abra o arquivo quiz.py, copie e cole o seguinte código no final do arquivo.

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


No agente, ele cria um analisador de saída JSON projetado especificamente para entender e estruturar a saída do LLM. Ele usa o modelo QuizQuestion que definimos anteriormente para garantir que a saída analisada esteja no formato correto (pergunta, opções e resposta).

👉 No terminal, execute os comandos a seguir para configurar um ambiente virtual, instalar dependências e iniciar o 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

👉 No menu "Visualização na Web" no canto superior direito, escolha Visualizar na porta 8080. O Cloud Shell vai abrir uma nova guia ou janela do navegador com a prévia da Web do seu aplicativo.

👉 No aplicativo da Web, clique no link "Quizzes", na barra de navegação superior ou no card da página de índice. Três testes gerados aleatoriamente vão aparecer para o estudante. Esses testes são baseados no plano de ensino e demonstram o poder do nosso sistema de geração de testes com tecnologia de IA.

Testes

👉Para interromper o processo em execução localmente, pressione Ctrl+C no terminal.

Gemini 2 Thinking para explicações

Ok, temos os testes, o que é um ótimo começo! Mas e se os estudantes errarem algo? É aí que o aprendizado de verdade acontece, certo? Se pudermos explicar por que a resposta estava errada e como chegar à correta, é muito mais provável que a pessoa se lembre dela. Além disso, isso ajuda a esclarecer dúvidas e aumentar a confiança.

Por isso, vamos usar o modelo de "raciocínio" do Gemini 2. É como dar à IA um pouco mais de tempo para pensar antes de explicar. Isso permite que ele dê um feedback mais detalhado e melhor.

Queremos saber se ele pode ajudar os estudantes com assistência, respostas e explicações detalhadas. Para testar, vamos começar com um assunto notoriamente complicado, o cálculo.

Visão geral

👉Primeiro, acesse o editor do Cloud Code, em answer.py dentro da pasta portal. Substitua o código da função a seguir

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

com o seguinte snippet de código:

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)

Este é um app langchain muito simples que inicializa o modelo Gemini 2 Flash, em que estamos instruindo-o a agir como um professor útil e fornecer explicações.

👉Execute o seguinte comando no terminal:

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

O resultado será semelhante ao exemplo das instruções originais. O modelo atual pode não fornecer uma explicação tão completa.

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

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

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

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

Keep practicing, you'll get there!

👉 No arquivo answer.py, substitua o

model_name de gemini-2.0-flash-001 para gemini-2.0-flash-thinking-exp-01-21 na função answer_thinking.

Isso muda o LLM para outro que funciona melhor com raciocínio. Isso ajuda o modelo a gerar explicações melhores.

👉 Execute o script answer.py novamente para testar o novo modelo de raciocínio:

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

Confira um exemplo de resposta do modelo de raciocínio muito mais completa e detalhada, com uma explicação de como resolver o problema de cálculo. Isso destaca o poder dos modelos de "pensamento" na geração de explicações de alta qualidade. Será exibida uma saída semelhante a esta:

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

👉REMOVA o seguinte código de teste de answer.py:

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

👉Execute os comandos a seguir no terminal para configurar um ambiente virtual, instalar dependências e iniciar o agente:

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

👉 No menu "Visualização na Web" no canto superior direito, escolha Visualizar na porta 8080. O Cloud Shell vai abrir uma nova guia ou janela do navegador com a prévia da Web do seu aplicativo.

👉 No aplicativo da Web, clique no link "Quizzes", na barra de navegação superior ou no card da página de índice.

👉 Responda a todos os testes e erre pelo menos uma resposta. Depois, clique em Enviar.

respostas de pensamento

Em vez de ficar olhando para a tela em branco enquanto espera a resposta, mude para o terminal do Cloud Editor. Você pode observar o progresso e as mensagens de saída ou de erro geradas pela função no terminal do emulador. 😁

👉 No terminal, pare o processo em execução localmente pressionando Ctrl+C.

11. OPCIONAL: orquestrar os agentes com o Eventarc

Até agora, o portal do estudante gerou testes com base em um conjunto padrão de planos de ensino. Isso é útil, mas significa que o agente de planejamento e o agente de teste do portal não estão realmente se comunicando. Lembra de como adicionamos o recurso em que o agente de planejamento publica os planos de ensino recém-gerados em um tópico do Pub/Sub? Agora é hora de conectar isso ao nosso agente do portal.

Visão geral

Queremos que o portal atualize automaticamente o conteúdo do teste sempre que um novo plano de ensino for gerado. Para isso, vamos criar um endpoint no portal que possa receber esses novos planos.

👉 No painel do Explorer do editor do Cloud Code, navegue até a pasta portal.

👉 Abra o arquivo app.py para edição. SUBSTITUA a linha ## REPLACE ME! NEW TEACHING PLAN pelo seguinte código:

@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

Recriar e implantar no Cloud Run

Você precisará atualizar e reimplantar os agentes de planejamento e portal no Cloud Run. Isso garante que eles tenham o código mais recente e estejam configurados para se comunicar por eventos.

Visão geral da implantação

👉Primeiro, vamos recriar e enviar a imagem do agente planner. No terminal, execute:

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

👉Vamos fazer o mesmo, criar e enviar a imagem do agente do 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

👉 Acesse o Artifact Registry. As imagens de contêiner aidemy-planner e aidemy-portal vão aparecer listadas em agent-repository.

Container Repo

👉De volta ao terminal, execute este comando para atualizar a imagem do Cloud Run do agente de planejamento:

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

Será exibida uma saída semelhante a esta:

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

Anote o URL do serviço, que é o link para o agente de planejamento implantado. Se você precisar determinar o URL do serviço do agente de planejamento mais tarde, use este 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)'

👉Execute este comando para criar a instância do Cloud Run para o agente portal.

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}

Será exibida uma saída semelhante a esta:

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

Anote o URL do serviço, que é o link para o portal do estudante implantado. Se você precisar determinar o URL do serviço do portal do estudante mais tarde, use este 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)'

Como criar o gatilho do Eventarc

Mas aqui está a grande questão: como esse endpoint é notificado quando há um plano atualizado esperando no tópico do Pub/Sub? É aí que o Eventarc entra em ação para salvar o dia!

O Eventarc atua como uma ponte, detectando eventos específicos (como uma nova mensagem chegando ao nosso tópico do Pub/Sub) e acionando automaticamente ações em resposta. No nosso caso, ele detecta quando um novo plano de ensino é publicado e envia um sinal para o endpoint do nosso portal, informando que é hora de atualizar.

Com o Eventarc processando a comunicação orientada a eventos, podemos conectar perfeitamente nosso agente de planejamento e o agente de portal, criando um sistema de aprendizado verdadeiramente dinâmico e responsivo. É como ter um mensageiro inteligente que entrega automaticamente os planos de aula mais recentes no lugar certo.

👉No console, acesse o Eventarc.

👉Clique no botão "+ CRIAR GATILHO".

Configurar o gatilho (noções básicas):

  • Nome do gatilho: plan-topic-trigger
  • Tipo de gatilho: origens do Google
  • Provedor de eventos: Cloud Pub/Sub
  • Tipo de evento: google.cloud.pubsub.topic.v1.messagePublished
  • Tópico do Cloud Pub/Sub: selecione projects/PROJECT_ID/topics/plan
  • Região: us-central1.
  • Conta de serviço:
    • CONCEDA à conta de serviço o papel roles/iam.serviceAccountTokenCreator
    • Use o valor padrão: conta de serviço de computação padrão
  • Destino do evento: Cloud Run
  • Serviço do Cloud Run: aidemy-portal
  • Ignore a mensagem de erro: "Permissão negada em "locations/me-central2" (ou ela pode não existir).
  • Caminho do URL do serviço: /new_teaching_plan

👉 Clique em "Criar".

A página "Acionadores do Eventarc" será atualizada, e o acionador recém-criado vai aparecer na tabela.

Agora, acesse o agente planner usando o URL do serviço dele para pedir um novo plano de ensino.

👉 Execute este comando no terminal para determinar o URL do serviço do agente planejador:

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

👉 Acesse o URL que foi gerado e tente Ano 5, Assunto Science e Solicitação de complemento atoms.

Aguarde um ou dois minutos. Esse atraso foi introduzido devido à limitação de faturamento deste laboratório. Em condições normais, não haveria atraso.

Por fim, acesse o portal do estudante usando o URL do serviço.

Execute este comando no terminal para determinar o URL do serviço do agente do portal do estudante:

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

Os testes foram atualizados e agora estão alinhados ao novo plano de ensino que você acabou de gerar. Isso demonstra a integração bem-sucedida do Eventarc no sistema Aidemy.

Aidemy: comemoração

Parabéns! Você criou um sistema multiagente no Google Cloud, aproveitando a arquitetura orientada a eventos para aumentar a escalonabilidade e a flexibilidade. Você já criou uma base sólida, mas ainda há muito mais para descobrir. Para saber mais sobre os benefícios reais dessa arquitetura, descubra o poder da API Live multimodal do Gemini 2 e aprenda a implementar a orquestração de caminho único com o LangGraph. Para isso, continue lendo os próximos dois capítulos.

12. OPCIONAL: Resumos em áudio com o Gemini

O Gemini pode entender e processar informações de várias fontes, como texto, imagens e até áudio, abrindo uma nova gama de possibilidades para aprendizado e criação de conteúdo. A capacidade do Gemini de "ver", "ouvir" e "ler" realmente abre portas para experiências do usuário criativas e envolventes.

Além de criar recursos visuais ou texto, outra etapa importante do aprendizado é o resumo e a recapitulação eficazes. Pense nisso: com que frequência você se lembra mais facilmente da letra de uma música cativante do que de algo que leu em um livro didático? O som pode ser muito memorável! Por isso, vamos aproveitar os recursos multimodais do Gemini para gerar resumos em áudio dos nossos planos de ensino. Assim, os estudantes terão uma maneira conveniente e interessante de revisar o material, o que pode aumentar a retenção e a compreensão com o poder do aprendizado auditivo.

Visão geral da API Live

Precisamos de um lugar para armazenar os arquivos de áudio gerados. O Cloud Storage oferece uma solução escalonável e confiável.

👉Acesse o Storage no console. Clique em "Buckets" no menu à esquerda. Clique no botão "+ CRIAR" na parte de cima.

👉Configure seu novo bucket:

  • nome do bucket: aidemy-recap-UNIQUE_NAME.
    • IMPORTANTE: defina um nome de bucket exclusivo que comece com aidemy-recap-. Esse prefixo exclusivo é essencial para evitar conflitos de nomenclatura ao criar seu bucket do Cloud Storage.
  • região: us-central1.
  • Classe de armazenamento: "Standard". A classe Standard é adequada para dados acessados com frequência.
  • Controle de acesso: deixe selecionado o controle de acesso padrão "Uniforme". Isso fornece controle de acesso consistente no nível do bucket.
  • Opções avançadas: para este workshop, as configurações padrão geralmente são suficientes.

Clique no botão CRIAR para criar o bucket.

  • Uma janela pop-up sobre a prevenção de acesso público pode aparecer. Deixe a caixa "Aplicar a prevenção do acesso público neste bucket" marcada e clique em Confirm.

O bucket recém-criado vai aparecer na lista "Buckets". Lembre-se do nome do bucket, porque você vai precisar dele depois.

👉No terminal do editor do Cloud Code, execute os comandos a seguir para conceder à conta de serviço acesso ao 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"

👉No editor do Cloud Code, abra audio.py na pasta courses. Cole o seguinte código no final do arquivo:

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))
  • Conexão de streaming: primeiro, uma conexão persistente é estabelecida com o endpoint da API Live. Ao contrário de uma chamada de API padrão, em que você envia uma solicitação e recebe uma resposta, essa conexão permanece aberta para uma troca contínua de dados.
  • Configuração multimodal: use a configuração para especificar o tipo de saída desejada (neste caso, áudio) e até mesmo os parâmetros que você quer usar (por exemplo, seleção de voz, codificação de áudio).
  • Processamento assíncrono: essa API funciona de forma assíncrona, ou seja, não bloqueia a linha de execução principal enquanto aguarda a conclusão da geração de áudio. Ao processar dados em tempo real e enviar a saída em partes, ele oferece uma experiência quase instantânea.

Agora, a questão principal é: quando esse processo de geração de áudio deve ser executado? O ideal é que os resumos em áudio estejam disponíveis assim que um novo plano de ensino for criado. Como já implementamos uma arquitetura orientada a eventos publicando o plano de ensino em um tópico do Pub/Sub, basta se inscrever nele.

No entanto, não geramos novos planos de ensino com muita frequência. Não seria eficiente ter um agente em execução constante esperando por novos planos. Por isso, faz sentido implantar essa lógica de geração de áudio como uma função do Cloud Run.

Ao implantá-la como uma função, ela permanece inativa até que uma nova mensagem seja publicada no tópico do Pub/Sub. Quando isso acontece, a função é acionada automaticamente, gerando os resumos em áudio e armazenando-os no nosso bucket.

👉Na pasta courses do arquivo main.py, esse arquivo define a função do Cloud Run que será acionada quando um novo plano de ensino estiver disponível. Ele recebe o plano e inicia a geração do resumo em áudio. Adicione o seguinte snippet de código ao final do arquivo.

@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: esse decorador marca a função como uma função do Cloud Run que será acionada por CloudEvents.

Como testar localmente

👉Vamos executar isso em um ambiente virtual e instalar as bibliotecas Python necessárias para a função do 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

👉O emulador de função do Cloud Run permite testar a função localmente antes de implantá-la no Google Cloud. Inicie um emulador local executando:

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

👉Enquanto o emulador estiver em execução, você poderá enviar CloudEvents de teste para simular a publicação de um novo plano de ensino. Em um novo terminal:

Dois terminais

👉Executar:

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

Em vez de ficar olhando para a tela em branco enquanto espera a resposta, mude para o outro terminal do Cloud Shell. Você pode observar o progresso e as mensagens de saída ou de erro geradas pela função no terminal do emulador. 😁

No segundo terminal, você verá que ele retornou OK.

👉Para verificar os dados no bucket, acesse Cloud Storage, selecione a guia "Bucket" e clique em aidemy-recap-UNIQUE_NAME

Bucket

👉No terminal que executa o emulador, digite ctrl+c para sair. e feche o segundo terminal. Feche o segundo terminal e execute "deactivate" para sair do ambiente virtual.

deactivate

Como implantar no Google Cloud

Visão geral da implantação 👉Depois de testar localmente, é hora de implantar o agente do curso no Google Cloud. No terminal, execute estes comandos:

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

Para verificar a implantação, acesse Cloud Run no console do Google Cloud.Um novo serviço chamado "courses-agent" vai aparecer.

Lista do Cloud Run

Para verificar a configuração do gatilho, clique no serviço "courses-agent" e veja os detalhes. Acesse a guia "TRIGGERS".

Um gatilho configurado para detectar mensagens publicadas no tópico do plano vai aparecer.

Gatilho do Cloud Run

Por fim, vamos ver como ele funciona de ponta a ponta.

👉Precisamos configurar o agente do portal para que ele saiba onde encontrar os arquivos de áudio gerados. No terminal, execute:

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

👉Gere um novo plano de ensino usando a página da Web do agente planejador. Pode levar alguns minutos para iniciar. Não se preocupe, é um serviço sem servidor.

Para acessar o agente de planejamento, execute o seguinte comando no terminal para receber o URL do serviço:

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

Depois de gerar o novo plano, aguarde de 2 a 3 minutos para que o áudio seja gerado. Isso vai levar mais alguns minutos devido à limitação de faturamento com esta conta do laboratório.

Para verificar se a função courses-agent recebeu o plano de ensino, consulte a guia "TRIGGERS" da função. Atualize a página periodicamente. Eventualmente, você vai notar que a função foi invocada. Se a função não for invocada após mais de dois minutos, tente gerar o plano de ensino novamente. No entanto, evite gerar planos repetidamente em rápida sucessão, já que cada plano gerado será consumido e processado sequencialmente pelo agente, o que pode criar um backlog.

Trigger Observe

👉Acesse o portal e clique em "Cursos". Você vai encontrar três cards, cada um mostrando uma recapitulação em áudio. Para encontrar o URL do seu agente do portal:

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

Clique em "play" em cada curso para garantir que os resumos em áudio estejam alinhados ao plano de ensino que você acabou de gerar. Cursos do portal

Saia do ambiente virtual.

deactivate

13. OPCIONAL: colaboração com base em papéis com o Gemini e o DeepSeek

Ter várias perspectivas é muito importante, principalmente ao criar atividades envolventes e reflexivas. Agora vamos criar um sistema multiagente que usa dois modelos diferentes com funções distintas para gerar atividades: um promove a colaboração e o outro incentiva o estudo individual. Vamos usar uma arquitetura "de disparo único", em que o fluxo de trabalho segue uma rota fixa.

Gerador de atividades do Gemini

Visão geral do Gemini Vamos começar configurando a função do Gemini para gerar atividades com ênfase na colaboração. Edite o arquivo gemini.py localizado na pasta assignment.

👉Cole o seguinte código no final do arquivo 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()

Ele usa o modelo do Gemini para gerar atividades.

Estamos prontos para testar o Agente do Gemini.

👉Execute estes comandos no terminal para configurar o 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

👉Você pode executar para testar:

python gemini.py

Você vai encontrar uma atividade com mais trabalho em grupo na saída. O teste de asserção no final também vai gerar os resultados.

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

Pare com ctl+c e limpe o código de teste. REMOVA o código a seguir de gemini.py

import unittest

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

        updated_state = gen_assignment_gemini(initial_state)

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


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

Configurar o gerador de atribuições do DeepSeek

Embora as plataformas de IA baseadas na nuvem sejam convenientes, o auto-hospedagem de LLMs pode ser crucial para proteger a privacidade e garantir a soberania dos dados. Vamos implantar o menor modelo do DeepSeek (1,5 bilhão de parâmetros) em uma instância do Cloud Compute Engine. Há outras maneiras, como hospedá-lo na plataforma Vertex AI do Google ou na sua instância do GKE, mas como este é apenas um workshop sobre agentes de IA e não quero que você fique aqui para sempre, vamos usar a maneira mais simples. Mas, se você tiver interesse e quiser conhecer outras opções, consulte o arquivo deepseek-vertexai.py na pasta de atividades. Ele fornece um exemplo de código de como interagir com modelos implantados na Vertex AI.

Visão geral do Deepseek

👉Execute este comando no terminal para criar uma plataforma LLM autohospedada 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

Para verificar se a instância do Compute Engine está em execução:

Acesse Compute Engine > "Instâncias de VM" no console do Google Cloud. O ollama-instance vai aparecer com uma marca de seleção verde indicando que está em execução. Se ela não aparecer, verifique se a zona é us-central1. Se não estiver, talvez seja necessário pesquisar.

Lista do Compute Engine

👉Vamos instalar o menor modelo do DeepSeek e testá-lo. No Editor do Cloud Shell, em um terminal Novo, execute o comando a seguir para se conectar via SSH à instância do GCE.

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

Ao estabelecer a conexão SSH, talvez você receba a seguinte solicitação:

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

Basta digitar Y(sem diferenciar maiúsculas e minúsculas) e pressionar Enter para continuar.

Em seguida, talvez seja necessário criar uma senha longa para a chave SSH. Se você preferir não usar uma senha, pressione Enter duas vezes para aceitar o padrão (sem senha).

👉Agora que você está na máquina virtual, extraia o menor modelo DeepSeek R1 e teste se ele funciona.

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

👉Saia da instância do GCE e digite o seguinte no terminal ssh:

exit

👉Em seguida, configure a política de rede para que outros serviços possam acessar o LLM. Limite o acesso à instância se quiser fazer isso para produção. Implemente o login de segurança para o serviço ou restrinja o acesso por IP. Execute:

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

👉Para verificar se a política de firewall está funcionando corretamente, execute:

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

Em seguida, vamos trabalhar na função Deepseek no agente de atividades para gerar atividades com ênfase no trabalho individual.

👉Edite deepseek.py na pasta assignment e adicione o seguinte snippet ao final:

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

👉Vamos testar executando:

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

Você vai encontrar uma atividade com mais trabalho de autoaprendizagem.

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

👉Pare o ctl+c e limpe o código de teste. REMOVA o código a seguir de deepseek.py

import unittest

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

        updated_state = gen_assignment_deepseek(initial_state)

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


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

Agora, vamos usar o mesmo modelo do Gemini para combinar as duas atividades em uma nova. Edite o arquivo gemini.py localizado na pasta assignment.

👉Cole o seguinte código no final do arquivo 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

Para combinar os pontos fortes dos dois modelos, vamos orquestrar um fluxo de trabalho definido usando o LangGraph. Esse fluxo de trabalho consiste em três etapas: primeiro, o modelo do Gemini gera uma atividade focada na colaboração; segundo, o modelo do DeepSeek gera uma atividade que enfatiza o trabalho individual; por fim, o Gemini sintetiza essas duas atividades em uma única atividade abrangente. Como predefinimos a sequência de etapas sem a tomada de decisões do LLM, isso constitui uma orquestração de caminho único definida pelo usuário.

Visão geral da combinação do Langraph

👉Cole o seguinte código no final do arquivo main.py na pasta 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()

👉Para testar inicialmente a função create_assignment e confirmar se o fluxo de trabalho que combina o Gemini e o DeepSeek está funcionando, execute o seguinte comando:

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

Você vai encontrar algo que combine os dois modelos com a perspectiva individual para estudo e para trabalhos em grupo.

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

👉Pare o ctl+c e limpe o código de teste. REMOVA o código a seguir de main.py

import unittest

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


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

Generate Assignment.png

Para tornar o processo de geração de atividades automático e responsivo a novos planos de ensino, vamos aproveitar a arquitetura orientada a eventos atual. O código a seguir define uma função do Cloud Run (generate_assignment) que será acionada sempre que um novo plano de ensino for publicado no tópico do Pub/Sub "plan".

👉Adicione o seguinte código ao final de main.py na pasta 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

Como testar localmente

Antes de implantar no Google Cloud, é recomendável testar a função do Cloud Run localmente. Isso permite uma iteração mais rápida e uma depuração mais fácil.

Primeiro, crie um bucket do Cloud Storage para armazenar os arquivos de atribuição gerados e conceda à conta de serviço acesso a ele. Execute os seguintes comandos no terminal:

👉IMPORTANTE: defina um nome exclusivo para ASSIGNMENT_BUCKET que comece com "aidemy-assignment-". Esse nome exclusivo é fundamental para evitar conflitos de nomenclatura ao criar seu bucket do Cloud Storage. (Substitua <YOUR_NAME> por qualquer palavra aleatória)

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

👉E execute:

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"

👉Agora, inicie o emulador de função do Cloud Run:

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

👉Enquanto o emulador estiver em execução em um terminal, abra um segundo terminal no Cloud Shell. Neste segundo terminal, envie um CloudEvent de teste para o emulador para simular a publicação de um novo plano de ensino:

Dois terminais

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

Em vez de ficar olhando para a tela em branco enquanto espera a resposta, mude para o outro terminal do Cloud Shell. Você pode observar o progresso e as mensagens de saída ou de erro geradas pela função no terminal do emulador. 😁

O comando curl vai imprimir "OK" (sem uma nova linha, então "OK" pode aparecer na mesma linha do prompt do shell do terminal).

Para confirmar se a atividade foi gerada e armazenada, acesse o console do Google Cloud e navegue até Armazenamento > "Cloud Storage". Selecione o bucket aidemy-assignment que você criou. Você vai encontrar um arquivo de texto chamado assignment-{random number}.txt no bucket. Clique no arquivo para fazer o download e verificar o conteúdo. Isso verifica se um novo arquivo contém uma nova atribuição recém-gerada.

12-01-assignment-bucket

👉No terminal que executa o emulador, digite ctrl+c para sair. e feche o segundo terminal. 👉Além disso, no terminal que executa o emulador, saia do ambiente virtual.

deactivate

Visão geral da implantação

👉Em seguida, vamos implantar o agente de atribuição na nuvem.

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 

Para verificar a implantação, acesse o console do Google Cloud e navegue até o Cloud Run. Você vai ver um novo serviço chamado "courses-agent" listado. 12-03-function-list

Com o fluxo de trabalho de geração de atividades implementado, testado e implantado, podemos passar para a próxima etapa: tornar essas atividades acessíveis no portal do estudante.

14. OPCIONAL: Colaboração com base em papéis com o Gemini e o DeepSeek (continuação)

Geração de sites dinâmicos

Para melhorar o portal do estudante e torná-lo mais interessante, vamos implementar a geração dinâmica de HTML nas páginas de atividades. O objetivo é atualizar automaticamente o portal com um design novo e visualmente atraente sempre que uma nova atividade for gerada. Isso aproveita os recursos de programação do LLM para criar uma experiência do usuário mais dinâmica e interessante.

14-01-generate-html

👉No editor do Cloud Shell, edite o arquivo render.py na pasta portal e substitua

def render_assignment_page():
    return ""

com o seguinte snippet de código:

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

Ele usa o modelo do Gemini para gerar HTML dinamicamente para a atividade. Ele usa o conteúdo da atividade como entrada e um comando para instruir o Gemini a criar uma página HTML visualmente atraente com um estilo criativo.

Em seguida, vamos criar um endpoint que será acionado sempre que um novo documento for adicionado ao bucket de atribuição:

👉Na pasta do portal, edite o arquivo app.py e SUBSTITUA a linha ## REPLACE ME! RENDER ASSIGNMENT pelo seguinte código:

@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 acionada, ela recupera o nome do arquivo e do bucket dos dados da solicitação, faz o download do conteúdo da atividade do Cloud Storage e chama a função render_assignment_page para gerar o HTML.

👉Vamos executar localmente:

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

👉No menu "Visualização na Web" na parte de cima da janela do Cloud Shell, selecione "Visualizar na porta 8080". Isso vai abrir o aplicativo em uma nova guia do navegador. Acesse o link Atribuição na barra de navegação. Neste ponto, você vai ver uma página em branco, o que é esperado, já que ainda não estabelecemos a ponte de comunicação entre o agente de atribuição e o portal para preencher o conteúdo de forma dinâmica.

14-02-deployment-overview

Para interromper o script, pressione Ctrl+C.

👉Para incorporar essas mudanças e implantar o código atualizado, recrie e envie a imagem do agente do 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 .
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

👉Depois de enviar a nova imagem, reimplante o serviço do Cloud Run. Execute o script a seguir para forçar a atualização do 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

👉Agora, vamos implantar um gatilho do Eventarc que detecta qualquer novo objeto criado (finalizado) no bucket de atribuição. Esse gatilho invoca automaticamente o endpoint /render_assignment no serviço do portal quando um novo arquivo de atividade é criado.

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"

Para verificar se o gatilho foi criado com sucesso, acesse a página Gatilhos do Eventarc no Console do Google Cloud. Você vai ver portal-assignment-trigger na tabela. Clique no nome do gatilho para ver os detalhes. Gatilho de atribuição

Pode levar de dois a três minutos para que o novo gatilho seja ativado.

Para ver a geração de atribuição dinâmica em ação, execute o comando a seguir para encontrar o URL do seu agente de planejamento (se você não tiver ele em mãos):

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

Encontre o URL do seu agente do portal:

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

No agente de planejamento, gere um novo plano de ensino.

13-02-assignment

Depois de alguns minutos (para permitir que a geração de áudio e de atividades e a renderização de HTML sejam concluídas), navegue até o portal do estudante.

👉Clique no link "Atividade" na barra de navegação. Uma atividade recém-criada com um HTML gerado dinamicamente vai aparecer. Cada vez que um plano de ensino é gerado, ele precisa ser uma atividade dinâmica.

13-02-assignment

Parabéns por concluir o sistema multiagente da Aidemy! Você ganhou experiência prática e insights valiosos sobre:

  • Os benefícios dos sistemas multiagente, incluindo modularidade, escalonabilidade, especialização e manutenção simplificada.
  • A importância das arquiteturas orientadas a eventos para criar aplicativos responsivos e com acoplamento flexível.
  • O uso estratégico de LLMs, combinando o modelo certo com a tarefa e integrando-os a ferramentas para impacto no mundo real.
  • Práticas de desenvolvimento nativo da nuvem usando serviços do Google Cloud para criar soluções escalonáveis e confiáveis.
  • A importância de considerar a privacidade de dados e os modelos de auto-hospedagem como uma alternativa às soluções de fornecedores.

Agora você tem uma base sólida para criar aplicativos sofisticados com tecnologia de IA no Google Cloud.

15. Desafios e próximas etapas

Parabéns por criar o sistema multiagente da Aidemy! Você criou uma base sólida para a educação com tecnologia de IA. Agora, vamos considerar alguns desafios e possíveis melhorias futuras para ampliar ainda mais os recursos e atender às necessidades do mundo real:

Aprendizagem interativa com perguntas e respostas ao vivo:

  • Desafio: você consegue usar a API Live do Gemini 2 para criar um recurso de perguntas e respostas em tempo real para estudantes? Imagine uma sala de aula virtual em que os estudantes podem fazer perguntas e receber respostas imediatas com tecnologia de IA.

Envio e avaliação automatizados de atividades:

  • Desafio: projetar e implementar um sistema que permita aos estudantes enviar atividades de forma digital e receber notas automáticas de uma IA, com um mecanismo para detectar e evitar plágio. Este desafio é uma ótima oportunidade para explorar a geração aumentada de recuperação (RAG) e melhorar a precisão e a confiabilidade dos processos de classificação e detecção de plágio.

aidemy-climb

16. Limpar

Agora que criamos e conhecemos nosso sistema multiagente da Aidemy, é hora de limpar o ambiente do Google Cloud.

👉Excluir serviços do 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

👉Excluir gatilho do 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

👉Excluir tópico do Pub/Sub

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

👉Excluir instância do Cloud SQL

gcloud sql instances delete aidemy --quiet

👉Excluir o repositório do Artifact Registry

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

👉Excluir secrets do Secret Manager

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

👉Excluir a instância do Compute Engine (se criada para o DeepSeek)

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

👉Excluir a regra de firewall da instância do Deepseek

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

👉Excluir buckets do Cloud Storage

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

aidemy-broom