Em uma postagem anterior, vimos o padrão de projeto ReAct para agentes inteligentes.

Porém, em alguns contextos, um agente de IA precisa de muito mais flexibilidade do que um “menu” fixo de ações como o ReAct possui.

Pense em um agente para análise de dados. Você pode criar algumas ferramentas para fazer consulta X específica e plotar um gráfico Y específico. Mas análise de dados é uma tarefa muito ampla, com muitas possibilidades de consultas e gráficos. Restringir a ferramentas fixas deixa o agente muito limitado.

O CodeAct surge como uma abordagem diferente e mais poderosa para este e outros cenários.

Neste artigo, vamos mostrar como criar um agente CodeAct completo para resolver um ambiente MiniGrid, como fizemos no artigo do ReAct.

Clique aqui para abrir o código deste artigo no Google Colab

A Ideia Central do CodeAct

A ideia é simples: em vez de escolher entre ações ou funções predefinidas, o agente escreve código Python, e o executa em seguida.

Se no ReAct o agente tem um “menu de ações” para escolher uma, no CodeAct, o agente troca esse menu fixo por uma linguagem de programação inteira.

Ou sejam em vez de escolher um ação pontual por vez, o agente age escrevendo um programa por vez.

Parece uma mudança pequena, mas abre muito mais espaço para o agente expressar estratégias complexas!

Neste artigo, vamos criar um agente CodeAct que gera um bloco de código Python, depois executa este código. Em seguida, o agente recebe como observação tudo o que tiver sido enviado para a saída padrão (stdout), ou seja, tudo que for impresso com print(). Este ciclo se repete até concluir a tarefa:

Escreve código → Executa → Captura saída padrão → Envia como observação ao agente → Repete

Em uma única rodada, no código gerado, o agente pode chamar várias funções, usar condicionais ou criar loops. Toda a lógica de programação fica disponível.

Porém, o agente deve imprimir as próprias observações: se ele não chamar print() no código gerado, não vai “ver” nada no próximo turno.

Tarefa de Exemplo: MiniGrid

Nosso agente vai ser criado para o MiniGrid, que é uma biblioteca que cria “ambientes” simples. Esses ambientes são como mini-jogos onde um agente precisa navegar por um mapa em forma de grade retangular para atingir um objetivo dado.

Curiosidade: O MiniGrid foi criado para treinar agentes automaticamente com Aprendizagem por Reforço. Mas vamos criar uma solução diferente aqui…

Animação do Minigrid

No MiniGrid, o agente pode se mover para frente, girar 90 graus para cada lado, abrir portas e fazer mais algumas ações (que não vamos usar). Cada episódio tem uma missão textual, como:

“Chegar ao objetivo evitando a lava”.

A biblioteca oferece mapas e objetivos diversos. Aqui, usaremos o MiniGrid-LavaCrossingS9N3-v0, onde o agente precisa chegar a uma célula objetivo, evitando células de “lava”.

Para saber mais sobre os ambientes disponíveis, veja a documentação oficial do MiniGrid.

Instalação e Configuração

O código completo contém todo o processo de instalação de módulos e de configuração, com explicações. Aqui, no artigo, vamos focar nas partes essenciais.

Um detalhe importante é que vamos usar um dos modelos Gemini, que podem ser usados gratuitamente.

Para isso, gere uma chave de API no Google AI Studio e salve nos Secrets do Colab com o nome GOOGLE_API_KEY. Se tiver dúvidas sobre como fazer isso, veja o post anterior sobre ReAct ou o post introdutório sobre o Google Gemini.

Códigos Auxiliares do Nosso Agente

1. Wrapper para Representação Textual

Foi criada uma classe auxiliar MiniGridTextWrapper que serve como wrapper. Ela converta as observações do MiniGrid para um mapa de texto legível, com orientação fixa. A classe completa está definida no código.

O que importa é entender o formato das observações que ela retorna. Cada célula do mapa é representada por um caractere, conforme a tabela abaixo:

Caractere Significado
# Parede
. Chão (célula vazia)
G Objetivo
L Lava
D Porta fechada (destrancada)
_ Porta aberta
X Porta trancada
> Agente virado para a direita
v Agente virado para baixo
< Agente virado para a esquerda
^ Agente virado para cima

2. Iniciando o Ambiente

Vamos usar uma variável global env para guardar a instância do ambiente MiniGrid devidamente encapsulada no wrapper.


env = gym.make("MiniGrid-LavaCrossingS9N3-v0")
env = MiniGridTextWrapper(env)

3. Funções de Ação

Uma chamada a env.step(NUM) executa alguma ação possível, onde cada número NUM corresponde a uma ação diferente (e.g. 2 representa “andar para a frente”).

Para tornar o código do agente mais legível, criamos funções especializadas que encapsulam essas chamadas com nomes descritivos:


def andar_para_frente():
    obs, r, termi, trunc, _ = env.step(2)
    return obs, r, (termi or trunc)

def girar_para_esquerda():
    obs, r, termi, trunc, _ = env.step(0)
    return obs, r, (termi or trunc)

def girar_para_direita():
    obs, r, termi, trunc, _ = env.step(1)
    return obs, r, (termi or trunc)

def abrir_porta():
    obs, r, termi, trunc, _ = env.step(5)
    return obs, r, (termi or trunc)

4. Testando o Ambiente

Vale explorar o ambiente manualmente antes de mostrar o código do agente. O código abaixo reseta o ambiente, e atribui a observação inicial à variável obs.

Em seguida, o código imprime a observação inicial. Note que ela é um dicionário com dois campos: mission (o objetivo em texto) e text_grid (o mapa textual).


obs, _ = env.reset()

print("Observação inicial:")
print("-- missão --")
print(obs['mission'])
print("-- estado do ambiente --")
print(obs['text_grid'])

Ao executar este código, você obterá algo assim:

Observação inicial do ambiente MiniGrid

Partes Centrais do Agente CodeAct

1. O System Prompt

O system prompt do CodeAct é mais longo que o do ReAct porque, além de descrever o ambiente, precisa ensinar ao modelo um tipo protocolo de comunicação via código:

SYSTEM_PROMPT_CODEACT = """
Você é um agente CodeAct controlando um ambiente Gymnasium MiniGrid via *funções de ação* Python.

# REGRAS
1. Responda APENAS com código entre <execute> e </execute>. Qualquer outro formato será rejeitado.
2. Variáveis disponíveis no ambiente: `obs`, `encerrado`, `recompensa` (resultado da última ação).
3. Toda ação deve ser chamada como: `obs, recompensa, encerrado = <função_de_ação>()`
4. Por turno: 1–5 chamadas de ação. Use `print()` para registrar o que precisar decidir no próximo turno.
5. Não precisa resolver em um turno. Comece simples — uma ação por turno inicialmente. Verifique `encerrado` entre ações.

# OBSERVAÇÃO (`obs`)
Dicionário com:
- `'mission'`: objetivo atual
- `'text_grid'`: mapa em grade de texto

Células: `#`=parede, `.`=chão, `D`=porta fechada, `_`=porta aberta, `G`=objetivo, `L`=lava
Agente: `^` `v` `<` `>` (posição + direção)

# AÇÕES (sem parâmetros)
- `andar_para_frente()` — move um quadrado à frente (se for `.`)
- `girar_para_esquerda()` — gira 90° anti-horário
- `girar_para_direita()` — gira 90° horário
- `abrir_porta()` — abre porta `D` à frente

Mover para fora da sala leva a outra sala.
"""

Dois pontos merecem destaque:

  1. O formato <execute>...</execute> é o contrato entre o modelo e o executor: tudo fora dessas tags é descartado.
  2. E o print() é um tipo de canal para “montar” a próxima observação.

Diferente do ReAct, o mapa não é informado automaticamente a cada passo. O agente precisa imprimir explicitamente o que quer ver na próxima observação.

2. Os Templates de Observação

Existe um template específico para a observação enviada para o agente no início, logo ao resetar o ambiente. Ela informa a missão (no campo {MISSION}) e o mapa inicial (campo {GRID}).

INITIAL_OBSERVATION_TEMPLATE = """
# OBSERVAÇÃO INICIAL

## mission
{MISSION}

## text_grid
{GRID}

Agora, *escreva código* para controlar o agente.
"""

Nos turnos seguintes, o agente CodeAct receberá apenas as saídas dos “prints” capturadas da execução anterior (campo {STDOUT_LOG}).

NEXT_OBSERVATION_TEMPLATE = """
NOVA OBSERVAÇÃO (STDOUT):
{STDOUT_LOG}
---
Agora, *escreva código* para controlar o agente.
"""

3. O Executor de Código

Este é o principal ponto de diferença em relação ao ReAct. Vamos precisar de uma função para executar código Python qualquer.

Python oferece a função exec(...), que executa qualquer string contendo o código Python.

Porém, criamos a função abaixo para chamar exec() e capturar tudo que for impresso na saída padrão durante a execução do código:


def run_codeact_snippet(code_str, exec_globals, exec_locals):
    stdout_buffer = io.StringIO()
    old_stdout = sys.stdout
    sys.stdout = stdout_buffer

    try:
        exec(code_str, exec_globals, exec_locals)
    finally:
        sys.stdout = old_stdout

    return stdout_buffer.getvalue()

O truque é redirecionar temporariamente o sys.stdout (saída padrão) para um buffer em memória (io.StringIO). Qualquer print() que acontecer durante na execução vai para esse buffer. Depois, o conteúdo do buffer é retornado como string.

4. O Loop Principal

Com todas as peças montadas, o loop fica como mostrado abaixo.

Alguns detalhes:

  • A variável next_message é usada para preparar o próximo prompt a ser enviado ao modelo.
  • A resposta textual do modelo é guardada em model_response_text. Dela, é extraído o código, que é guardado em code.
  • O código é executado ao chamar a função run_codeact_snippet, explicada antes.
  • O dicionário exec_context serve para informar quais variáveis e funções podem ser usadas e até alteradas ao executar o código.
  • Após a execução, os novos valores das variáveis obs, encerrado e recompensa são lidos do mesmo dicionário exec_context.

# Reinicia o ambiente
obs, _ = env.reset()
encerrado = False
recompensa = 0.0

# Contexto de execução — variáveis e funções acessíveis pelo código do modelo
exec_context = {
    "obs": obs,
    "encerrado": encerrado,
    "recompensa": recompensa,
    "andar_para_frente": andar_para_frente,
    "girar_para_esquerda": girar_para_esquerda,
    "girar_para_direita": girar_para_direita,
    "abrir_porta": abrir_porta
}

# Cria a sessão de chat com o modelo
chat = GEMINI_CLIENT.chats.create(
    model=GEMINI_MODEL_ID,
    config={'system_instruction': SYSTEM_PROMPT_CODEACT, 'temperature': 0.3}
)

turn_num = 0
next_message = INITIAL_OBSERVATION_TEMPLATE.format(
    MISSION=obs["mission"], GRID=obs["text_grid"]
)

while not encerrado and turn_num < 20:
    # 1. Envia a mensagem ao modelo e recebe o código
    model_response = chat.send_message(next_message)
    model_response_text = model_response.text

    # 2. Extrai o bloco <execute>...</execute>
    match = re.search(r"<execute>(.*?)</execute>", model_response_text, re.DOTALL)

    if not match:
        next_message = ("Sua resposta não seguiu o formato exigido.\n"
                        "Responda com código Python entre <execute> e </execute>.")
        continue

    code = match.group(1)

    # 3. Executa o código e captura o stdout
    try:
        stdout_output = run_codeact_snippet(code, {}, exec_context)
        turn_num += 1
    except Exception as e:
        next_message = f"Erro de execução:\n{e}"
        continue

    # 4. Atualiza variáveis do loop principal a partir do exec_context
    obs        = exec_context["obs"]
    encerrado  = exec_context["encerrado"]
    recompensa = exec_context["recompensa"]

    # 5. Prepara a próxima mensagem com o stdout capturado
    next_message = NEXT_OBSERVATION_TEMPLATE.format(STDOUT_LOG=stdout_output)
    time.sleep(5.0)


# Resultado final
if recompensa > 0:
    print("\nSUCESSO: O modelo chegou ao objetivo!")
else:
    print("\nFALHA: O agente morreu ou atingiu o limite de passos!")

env.close()

O loop tem três mecanismos de segurança para lidar com situações inesperadas:

  1. Se as tags <execute> estiverem faltando, o modelo recebe um aviso e tenta de novo.
  2. Se o código lançar uma exceção, a mensagem de erro volta como observação e o modelo pode corrigir no turno seguinte.
  3. E se o agente não conseguir avançar na solução do ambiente, o limite de 20 turnos encerra o episódio.

Conclusão

No padrão CodeAct, o modelo não só age, ele decide o que observar e também como estruturar a própria estratégia e até como se recuperar de erros. De certa forma, o agente deixa de ser um executor de instruções e passa a ser um programador resolvendo um problema em tempo real.

Clique aqui para abrir o código deste artigo no Google Colab