Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Implementar persistência de memória em arquivo JSON local (carregar e salvar)
Construir tools de pesquisa, notas e relatório com estado persistente entre sessões
Gerar relatório Markdown estruturado com notas agrupadas por tag
Completar os 5 TODOs do agente de pesquisa autônomo
Integrar todos os padrões anteriores (agent loop, tools, memória, logs) em um projeto coeso
Por que isso importa
Esta unidade integra tudo o que você aprendeu no módulo. O agente de pesquisa: - Tem memory persistente (JSON local — padrão para prototipagem, vector DB em produção) - Usa tools especializadas (buscar, anotar, listar, gerar relatório) - Persiste entre sessões (rodar novamente usa as notas da sessão anterior) - Gera output estruturado (relatório Markdown pronto para compartilhar)
Em produção, esse padrão é a base de assistentes de pesquisa, sistemas de knowledge management e copilotas de inteligência competitiva.
Conceitos Fundamentais
Memória Persistente com JSON
O padrão mais simples de persistência: um arquivo JSON no disco. Sem banco de dados, sem dependências.
from pathlib import Path
import json
MEMORIA_FILE = Path('memoria_agente.json')
# TODO 1: Carregar
def carregar_memoria() -> dict:
if MEMORIA_FILE.exists():
with open(MEMORIA_FILE, encoding='utf-8') as f:
return json.load(f)
return {'notas': []} # estrutura inicial vazia
# TODO 2: Salvar
def salvar_memoria(memoria: dict):
with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:
json.dump(memoria, f, indent=2, ensure_ascii=False)
ensure_ascii=False preserva acentos portugueses no arquivo. indent=2 torna o JSON legível quando você abrir com um editor.TODO 3: salvar_nota() com Persistência
def salvar_nota(titulo: str, conteudo: str, tags: list[str] = None, memoria: dict = None) -> str:
nota = {
'id': len(memoria.get('notas', [])) + 1,
'titulo': titulo,
'conteudo': conteudo,
'tags': tags or [],
'criado_em': datetime.utcnow().isoformat(),
}
# TODO 3: adicionar e persistir
memoria['notas'].append(nota)
salvar_memoria(memoria) # persiste imediatamente
return f'Nota "{titulo}" salva com id={nota["id"]}'Persistir imediatamente (não só ao final) garante que mesmo se o agente for interrompido no meio, as notas já estão salvas.
TODO 4: gerar_relatorio() em Markdown
def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:
notas = memoria.get('notas', [])
data_atual = datetime.utcnow().strftime('%Y-%m-%d')
# Agrupar por tag
por_tag: dict[str, list] = {}
sem_tag = []
for nota in notas:
if nota.get('tags'):
for tag in nota['tags']:
por_tag.setdefault(tag, []).append(nota)
else:
sem_tag.append(nota)
# Construir Markdown
linhas = [
f'# {titulo}',
f'',
f'*Gerado em: {data_atual}*',
f'',
f'## Sumário',
f'',
f'- Total de notas: {len(notas)}',
f'- Tags: {", ".join(sorted(por_tag.keys())) or "nenhuma"}',
f'',
]
for tag, notas_tag in sorted(por_tag.items()):
linhas.append(f'## {tag.title()}')
linhas.append('')
for nota in notas_tag:
linhas.append(f'### [{nota["id"]}] {nota["titulo"]}')
linhas.append('')
linhas.append(nota['conteudo'])
linhas.append('')
if sem_tag:
linhas.append('## Sem Categoria')
linhas.append('')
for nota in sem_tag:
linhas.append(f'### {nota["titulo"]}')
linhas.append(nota['conteudo'])
linhas.append('')
linhas.extend(['## Conclusão', '', conclusao])
return '\n'.join(linhas)TODO 5: Resumo Final
# No final de rodar_agente():
notas_salvas = len(memoria.get('notas', []))
print(f'\n[MEMÓRIA] {notas_salvas} nota(s) salva(s) em {MEMORIA_FILE}')
# Estimativa de custo (opcional mas útil)
# O agente não acumula custo explicitamente no starter, mas podemos estimar
# com base no número de iterações × custo médio por iteraçãoAprofundamento Técnico
O Ciclo Completo de Sessões
Sessão 1:
carregar_memoria() → {'notas': []} (arquivo não existe)
agente pesquisa → salva 3 notas
salvar_memoria() → cria memoria_agente.json
Sessão 2:
carregar_memoria() → {'notas': [nota1, nota2, nota3]} (carrega da sessão anterior)
agente pesquisa → salva mais 2 notas
salvar_memoria() → atualiza com 5 notas totais
Sessão 3:
listar_notas(tag='ia-generativa') → filtra por tag
gerar_relatorio(...) → combina todas as notas em MarkdownIsso é um knowledge accumulator — o agente fica mais rico com cada sessão.
Sistema de Tags para Organização
def listar_notas(memoria: dict, tag: str = None) -> str:
notas = memoria.get('notas', [])
if tag:
notas = [n for n in notas if tag in n.get('tags', [])]
if not notas:
return f'Nenhuma nota encontrada{f" com tag \'{tag}\'" if tag else ""}.'
return '\n'.join(
f'[{n["id"]}] {n["titulo"]} — tags: {n.get("tags", [])} — {n["criado_em"][:10]}'
for n in notas
)O agente pode chamar listar_notas(tag='rag') antes de pesquisar para verificar se já tem notas sobre o tema.
Tool de Relatório como Tool “Terminal”
A tool gerar_relatorio é uma “terminal tool” — quando o agente a chama, indica que a pesquisa está completa. O system prompt instrui:
system='Você é um agente de pesquisa. Use as tools para pesquisar, anotar descobertas '
'e gerar um relatório completo ao final.'“ao final” instrui o agente a chamar gerar_relatorio como última ação.
Exemplos Anotados
Exemplo 1: Todos os TODOs Resolvidos
from pathlib import Path
from datetime import datetime
import json
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))
MEMORIA_FILE = Path('memoria_agente.json')
# TODO 1: carregar
def carregar_memoria() -> dict:
if MEMORIA_FILE.exists():
with open(MEMORIA_FILE, encoding='utf-8') as f:
return json.load(f)
return {'notas': []}
# TODO 2: salvar
def salvar_memoria(memoria: dict):
with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:
json.dump(memoria, f, indent=2, ensure_ascii=False)
# TODO 3: adicionar nota e persistir
def salvar_nota(titulo: str, conteudo: str, tags: list[str] = None, memoria: dict = None) -> str:
nota = {
'id': len(memoria.get('notas', [])) + 1,
'titulo': titulo,
'conteudo': conteudo,
'tags': tags or [],
'criado_em': datetime.utcnow().isoformat(),
}
memoria['notas'].append(nota)
salvar_memoria(memoria)
return f'Nota "{titulo}" salva com id={nota["id"]}'
# TODO 4: gerar relatório Markdown
def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:
notas = memoria.get('notas', [])
data = datetime.utcnow().strftime('%Y-%m-%d')
por_tag: dict[str, list] = {}
sem_tag = []
for n in notas:
if n.get('tags'):
for t in n['tags']:
por_tag.setdefault(t, []).append(n)
else:
sem_tag.append(n)
md = [f'# {titulo}', f'', f'*{data} | {len(notas)} notas*', f'']
for tag, ns in sorted(por_tag.items()):
md.append(f'## {tag.replace("-", " ").title()}')
md.append('')
for n in ns:
md.append(f'### {n["titulo"]}')
md.append('')
md.append(n['conteudo'])
md.append('')
md.extend(['## Conclusão', '', conclusao])
return '\n'.join(md)
def rodar_agente(tarefa: str):
memoria = carregar_memoria()
mensagens = [{'role': 'user', 'content': tarefa}]
max_iter = 10
print(f'\n[AGENTE] Tarefa: {tarefa[:100]}')
print(f'[MEMÓRIA] {len(memoria.get("notas", []))} nota(s) existente(s)')
for iteracao in range(1, max_iter + 1):
print(f'\n[Iter {iteracao}]')
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1500,
tools=TOOLS,
system='Você é um agente de pesquisa. Pesquise, anote descobertas e gere relatório ao final.',
messages=mensagens,
)
if response.stop_reason == 'end_turn':
texto = next((b.text for b in response.content if hasattr(b, 'text')), '')
print(f'\n[CONCLUÍDO] {texto[:200]}')
break
tool_use_blocks = [b for b in response.content if b.type == 'tool_use']
if not tool_use_blocks:
break
mensagens.append({'role': 'assistant', 'content': response.content})
tool_results = []
for block in tool_use_blocks:
print(f' → {block.name}({list(block.input.keys())})')
resultado = executar_tool(block.name, block.input, memoria)
print(f' ← {resultado[:100]}')
tool_results.append({'type': 'tool_result', 'tool_use_id': block.id, 'content': resultado})
mensagens.append({'role': 'user', 'content': tool_results})
# TODO 5: resumo final
notas_salvas = len(memoria.get('notas', []))
print(f'\n[MEMÓRIA] {notas_salvas} nota(s) salva(s) em {MEMORIA_FILE}')Padrões e Armadilhas
Padrões
Padrão 1: Persistir nota imediatamente após criação
memoria['notas'].append(nota)
salvar_memoria(memoria) # imediatamente — não apenas ao finalSe o agente for interrompido, notas já escritas são preservadas.
Padrão 2: ID sequencial baseado em tamanho da lista
'id': len(memoria.get('notas', [])) + 1Simples e incrementa automaticamente. Em produção, use uuid4() para evitar conflitos em acesso concorrente.
Padrão 3: ensure_ascii=False + indent=2 em json.dump
json.dump(memoria, f, indent=2, ensure_ascii=False)Arquivo legível por humanos + suporte a português.
Armadilhas
⚠️ Armadilha 1: Carregar sem checar existência do arquivo
# ERRADO: FileNotFoundError se MEMORIA_FILE não existe
with open(MEMORIA_FILE) as f:
return json.load(f)
# CORRETO:
if MEMORIA_FILE.exists():
with open(MEMORIA_FILE) as f:
return json.load(f)
return {'notas': []}⚠️ Armadilha 2: executar_tool precisa receber memoria como parâmetro
def executar_tool(nome: str, args: dict, memoria: dict) -> str:
if nome == 'salvar_nota':
return salvar_nota(args['titulo'], args['conteudo'], args.get('tags', []), memoria)
# ...A memoria deve fluir por referência para que salvar_nota modifique a instância correta.
⚠️ Armadilha 3: Relatório gerado com memoria desatualizada
# ERRADO: memoria carregada no início pode não ter notas salvas nesta sessão
# se salvar_memoria() não foi chamado corretamente
def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:
notas = memoria.get('notas', []) # ← usa a mesma instância que foi modificadaComo memoria é passada por referência e salvar_nota modifica memoria['notas'], o relatório sempre tem as notas mais recentes.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
5 TODOs mapeados diretamente:
TODO 1 — carregar_memoria():
if MEMORIA_FILE.exists():
with open(MEMORIA_FILE, encoding='utf-8') as f:
return json.load(f)
return {'notas': []}TODO 2 — salvar_memoria():
with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:
json.dump(memoria, f, indent=2, ensure_ascii=False)TODO 3 — em salvar_nota(), após criar nota:
memoria['notas'].append(nota)
salvar_memoria(memoria)TODO 4 — gerar_relatorio(): implementar o Markdown com agrupamento por tag (veja Exemplo 1 desta aula).
TODO 5 — após o loop em rodar_agente():
print(f'\n[MEMÓRIA] {len(memoria.get("notas", []))} nota(s) salva(s) em {MEMORIA_FILE}')Agora você está pronto para o lab.