🎓 Aula 04 – Memória de Agente com LangChain + Pinecone
🎓 Aula 04 – Memória de Agente com LangChain + Pinecone

Agentes
Voltar para página principal do blog
Todas as aulas desse curso
Aula 02 Aula 04
Redes Sociais do Código Fluente:
Scarlett Finch
Scarlett Finch é uma influenciadora virtual criada com IA.
Ela é 🎤 cantora e 🎶compositora pop britânica.
Siga a Scarlett Finch no Instagram:
Conecte-se comigo!
LinkedIn: Fique à vontade para me adicionar no LinkedIn.
Ao conectar-se comigo, você terá acesso a atualizações regulares sobre desenvolvimento web, insights profissionais e oportunidades de networking no setor de tecnologia.
GitHub: Siga-me no GitHub para ficar por dentro dos meus projetos mais recentes, colaborar em código aberto ou simplesmente explorar os repositórios que eu contribuo, o que pode ajudar você a aprender mais sobre programação e desenvolvimento de software.
Recursos e Afiliados
Explorando os recursos abaixo, você ajuda a apoiar nosso site.
Somos parceiros afiliados das seguintes plataformas:
- https://heygen.com/ – Eleve a produção de seus vídeos com HeyGen! Com esta plataforma inovadora, você pode criar vídeos envolventes utilizando avatares personalizados, ideal para quem busca impactar e conectar com audiências em todo o mundo. HeyGen transforma a maneira como você cria conteúdo, oferecendo ferramentas fáceis de usar para produzir vídeos educativos, demonstrações de produtos e muito mais. Descubra o poder de comunicar através de avatares interativos e traga uma nova dimensão para seus projetos. Experimente HeyGen agora e revolucione sua forma de criar vídeos!
- letsrecast.ai – Redefina a maneira como você consome artigos com Recast. Esta plataforma transforma artigos longos em diálogos de áudio que são informativos, divertidos e fáceis de entender. Ideal para quem está sempre em movimento ou busca uma forma mais conveniente de se manter informado. Experimente Recast agora.
- dupdub.com – Explore o universo do marketing digital com DupDub. Esta plataforma oferece ferramentas inovadoras e soluções personalizadas para elevar a sua estratégia de marketing online. Ideal para empresas que buscam aumentar sua visibilidade e eficiência em campanhas digitais. Descubra mais sobre DupDub.
- DeepBrain AI Studios – Revolucione a criação de conteúdo com a tecnologia de inteligência artificial da DeepBrain AI Studios. Esta plataforma avançada permite que você crie vídeos interativos e apresentações utilizando avatares digitais gerados por IA, que podem simular conversas reais e interações humanas. Perfeito para educadores, criadores de conteúdo e empresas que querem inovar em suas comunicações digitais. Explore DeepBrain AI Studios.
- Audyo.ai – Transforme a maneira como você interage com conteúdo auditivo com Audyo.ai. Esta plataforma inovadora utiliza inteligência artificial para criar experiências de áudio personalizadas, melhorando a acessibilidade e a compreensão de informações através de podcasts, transcrições automáticas e síntese de voz avançada. Ideal para profissionais de mídia, educadores e qualquer pessoa que deseje acessar informações auditivas de maneira mais eficiente e envolvente. Descubra Audyo.ai e suas possibilidades.
- Acoust.io – Transforme sua produção de áudio com Acoust.io. Esta plataforma inovadora fornece uma suite completa de ferramentas para criação, edição e distribuição de áudio, ideal para artistas, produtores e empresas de mídia em busca de excelência e inovação sonora. Acoust.io simplifica o processo de levar suas ideias à realidade, oferecendo soluções de alta qualidade que elevam seus projetos de áudio. Experimente Acoust.io agora e descubra um novo patamar de possibilidades para seu conteúdo sonoro.
- Hostinger – Hospedagem web acessível e confiável. Ideal para quem busca soluções de hospedagem de sites com excelente custo-benefício e suporte ao cliente robusto. Saiba mais sobre a Hostinger.
- Digital Ocean – Infraestrutura de nuvem para desenvolvedores. Oferece uma plataforma de nuvem confiável e escalável projetada especificamente para desenvolvedores que precisam de servidores virtuais, armazenamento e networking. Explore a Digital Ocean.
- One.com – Soluções simples e poderosas para o seu site. Uma escolha ideal para quem busca registrar domínios, hospedar sites ou criar presença online com facilidade e eficiência. Visite One.com.
Educação e Networking
Amplie suas habilidades e sua rede participando de cursos gratuitos e comunidades de desenvolvedores:
- Digital Innovation One – Cursos gratuitos com certificado.
- Workover – Aprenda Python3 gratuitamente.
- Comunidades de desenvolvedores para networking:
Canais do Youtube
Explore nossos canais no YouTube para uma variedade de conteúdos educativos e de entretenimento, cada um com um foco único para enriquecer sua experiência de aprendizado e lazer.
Toti
Toti: Meu canal pessoal, onde posto clips artesanais de músicas que curto tocar, dicas de teoria musical, entre outras coisas.
Scarlett Finch
Scarlett Finch: Cantora e influenciadora criada com IA.
Lofi Music Zone Beats
Lofi Music Zone Beats: O melhor da música Lofi para estudo, trabalho e relaxamento, criando o ambiente perfeito para sua concentração.
Backing Track / Play-Along
Backing Track / Play-Along: Acompanhe faixas instrumentais para prática musical, ideal para músicos que desejam aprimorar suas habilidades.
Código Fluente
Código Fluente: Aulas gratuitas de programação, devops, IA, entre outras coisas.
Putz!
Putz!: Canal da banda Putz!, uma banda virtual, criada durante a pandemia com mais 3 amigos, Fábio, Tatá e Lula.
PIX para doações

PIX Nubank
🎓 Aula 04 – Memória de Agente com LangChain + Pinecone
Código da aula: Github
🧭 Introdução
Na aula anterior, construímos o Rafa, nosso agente vendedor do livro NeuroZen.
Ele conversa bem, aplica gatilhos mentais e captura e-mails. Mas tem um problema sério: ele esquece tudo.
Cada vez que o usuário abre a página, o Rafa não faz ideia de quem está do outro lado. É como contratar um vendedor com amnésia total.
Nesta aula vamos resolver isso. Vamos dar memória real ao nosso agente usando duas ferramentas poderosas e 100% gratuitas: LangChain e Pinecone.
O novo agente se chama Memo — e o nome não é à toa.
Ao contrário do Rafa, o Memo vai lembrar o nome do visitante, os tópicos que ele perguntou, suas dúvidas anteriores, e usar tudo isso para personalizar a conversa — exatamente como um bom vendedor humano faria.
E diferente de soluções que rodam só na sua máquina, tudo nesta aula roda na nuvem — Pinecone para a memória vetorial e Render para o servidor Python. Gratuitos, sem cartão de crédito, acessíveis de qualquer lugar.
Você vai sair desta aula com um link público funcionando de verdade.
🧠 Os Dois Tipos de Memória de um Agente
Antes de codar, precisamos entender um conceito fundamental que vai aparecer em todas as próximas aulas: agentes inteligentes têm dois tipos distintos de memória, assim como os humanos.
Memória de Curto Prazo (In-Context)
É o histórico da conversa atual, mantido dentro do próprio prompt enviado ao LLM.
Funciona enquanto a janela de contexto do modelo aguenta.
É o que já fazíamos antes de forma implícita: a cada mensagem, mandávamos todo o histórico junto.
Limitação: quando o usuário fecha e reabre a página, acabou. Tudo zerado.
Memória de Longo Prazo (External Store)
É onde entra o Pinecone.
As informações são convertidas em vetores numéricos (embeddings) e armazenadas na nuvem.
Na próxima sessão, o agente faz uma busca semântica — encontra os contextos mais relevantes — e os injeta no prompt.
Esse é o padrão RAG (Retrieval-Augmented Generation).
A grande sacada: você não precisa guardar tudo.
O agente busca apenas o que é relevante para a pergunta atual.
Se o usuário perguntar sobre preço, o sistema recupera os fragmentos de memória onde preço foi discutido antes — não o histórico completo.
⚙️ Tecnologias da Aula
LangChain
Um framework Python que funciona como “cola” entre LLMs, ferramentas de memória, bancos de dados vetoriais e muito mais. Ele abstrai a complexidade de orquestrar esses componentes.
Pinecone
Banco de dados vetorial na nuvem com plano gratuito permanente.
Não precisa instalar nada, não precisa de servidor — você cria uma conta, gera uma API key e já está pronto para usar.
É o mais adotado pelo mercado para projetos com busca semântica e RAG.
FastEmbed
Biblioteca da Qdrant que converte texto em vetores numéricos (embeddings) no servidor, sem precisar de API paga e sem depender do PyTorch. Vamos usar o modelo sentence-transformers/all-MiniLM-L6-v2, que roda via ONNX — muito mais leve e rápido para iniciar no Render.
FastAPI + Render
O FastAPI expõe o agente como uma API REST.
O Render hospeda esse servidor na nuvem gratuitamente, sem cartão de crédito, com deploy direto do GitHub e URL pública incluída.
Groq (continuando da aula anterior)
Mantemos o Groq para a geração de respostas — gratuito, rápido, e agora conectado a um sistema de memória real na nuvem.
🔑 Criando as Contas Necessárias
Antes de codar, crie as contas gratuitas nas três plataformas:
Pinecone
- Acesse pinecone.io e clique em Sign Up Free
- Faça login e vá em API Keys no menu lateral
- Copie a chave gerada — você vai usá-la como
PINECONE_API_KEY - Anote também a região do seu projeto (ex:
us-east-1) — aparece na mesma tela
O plano gratuito permite 1 index ativo com até 100.000 vetores — mais do que suficiente para este projeto.
Render
- Acesse render.com e clique em Get Started for Free
- Crie a conta com seu GitHub — isso facilita o deploy depois
O plano gratuito do Render não exige cartão de crédito e inclui SSL e URL pública no domínio .onrender.com.
A única limitação: o servidor dorme após uma semana sem receber requisições — o primeiro acesso após isso demora cerca de 50 segundos para “acordar”. Para uma aula isso não é problema.
Groq (se ainda não tiver)
- Acesse console.groq.com
- Crie a conta e gere uma API Key em API Keys
📁 Estrutura do Projeto
Este projeto usa uma estrutura de monorepo — frontend e backend ficam na mesma pasta do repositório, mas são deployados de forma independente. O backend vai para o Render, o frontend vai para o Netlify.
neurozen-memo-agent/
├── backend/
│ ├── memory_agent.py ← Agente principal com memória
│ ├── server.py ← API FastAPI
│ ├── test_memory.py ← Script para testar a memória
│ ├── inspect_db.py ← Script para inspecionar o Pinecone
│ └── requirements.txt ← Dependências Python
├── frontend/
│ ├── index.html ← Landing page do NeuroZen com chat do Memo
│ ├── styles.css ← Estilos globais
│ ├── thanks.html ← Página de agradecimento
│ ├── privacy-policy.html ← Política de privacidade
│ ├── terms-of-use.html ← Termos de uso
│ └── js/
│ ├── agent.js ← Stub de compatibilidade
│ ├── chat.js ← Comunicação com a API na nuvem
│ ├── ui.js ← Controle visual das mensagens
│ └── form.js ← Captura de e-mails no Google Sheets
├── netlify.toml ← Configuração do deploy no Netlify
├── .gitignore
└── README.mdAntes de seguir
O código completo deste projeto já está disponível no repositório do GitHub — se preferir, clone direto e pule para a parte de configuração das chaves de API.
Mas vamos seguir aqui com o passo a passo completo, construindo cada arquivo do zero, porque entender o que cada peça faz é mais importante do que ter o projeto funcionando rápido.
🌐 Frontend — Reaproveitando o Projeto do Rafa
O frontend deste projeto é basicamente o mesmo da aula anterior — mesma landing page do NeuroZen, mesmo livro, mesma estrutura de arquivos. A diferença é que agora o agente que atende o visitante é o Memo, que tem memória real.
Do ponto de vista do visitante, a experiência é a mesma. Por baixo dos panos, é completamente diferente.
Copie os seguintes arquivos do projeto da Aula 03 para a pasta frontend/:
index.htmlstyles.cssthanks.htmlprivacy-policy.htmlterms-of-use.htmljs/ui.jsjs/form.jsimages/(pasta com a capa do livro)
Apenas dois arquivos JavaScript são novos e substituem os da aula anterior: agent.js e chat.js.
frontend/js/agent.js
O prompt do Memo vive no servidor Python. Este arquivo existe apenas para manter a compatibilidade com a estrutura de imports do projeto.
// O prompt do Memo é gerenciado pelo servidor Python (memory_agent.py)
// Este arquivo existe para manter a compatibilidade com a estrutura do projeto
export const agentPrompt = "";
frontend/js/chat.js
O chat se comunica com o servidor hospedado no Render. O session_id é gerado uma vez e persistido no localStorage do navegador, garantindo que o Memo reconheça o usuário mesmo após fechar e reabrir a aba.
Substitua a constante API_URL pelo endereço real do seu serviço no Render antes de fazer o deploy do frontend.
import { agentPrompt } from "./agent.js";
import { displayMessage, showTyping, removeTyping } from "./ui.js";
// URL do servidor no Render — substitua pela sua URL real após o deploy
const API_URL = "https://neurozen-memo-agent.onrender.com";
// Espera o DOM carregar
window.addEventListener("DOMContentLoaded", () => {
const chatMessages = document.getElementById("chat-messages");
const sendBtn = document.getElementById("send-btn");
const userInput = document.getElementById("user-input");
let isMemoTyping = false;
let shouldPauseMemo = false;
const welcomeSequences = [
[
"Oi! 👋 Eu sou o Memo, tudo bem?",
"Faço parte da equipe do NeuroZen e conheço bem o livro.",
"Se tiver dúvidas ou quiser saber se ele é pra você, estou por aqui!"
],
[
"Olá! 😊 Seja bem-vindo ao NeuroZen.",
"Meu nome é Memo, li o livro do começo ao fim.",
"Posso te ajudar a entender se ele é o que você está procurando."
],
[
"E aí! 👋 Tudo certo?",
"Sou o Memo da equipe do NeuroZen. Já mergulhei no conteúdo do livro.",
"Quer trocar uma ideia e ver se ele faz sentido pra você?"
]
];
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
userInput.addEventListener("input", () => {
if (isMemoTyping) shouldPauseMemo = true;
});
async function showWelcomeMessage() {
const sequence = welcomeSequences[Math.floor(Math.random() * welcomeSequences.length)];
for (let i = 0; i < sequence.length; i++) {
const msg = sequence[i];
const typingTime = 30 * msg.length + 500;
if (i !== 0) await delay(600 + Math.random() * 700);
showTyping(chatMessages);
await delay(typingTime);
removeTyping(chatMessages);
displayMessage(chatMessages, msg, "bot");
}
}
function splitResponse(response) {
const sentences = response
.split(/(?<=[.!?])\s+|\n+/)
.filter(sentence => sentence.trim().length > 0);
const messages = [];
let currentMessage = "";
for (const sentence of sentences) {
if (currentMessage.length + sentence.length > 120 && currentMessage.length > 0) {
messages.push(currentMessage.trim());
currentMessage = sentence;
} else {
currentMessage += (currentMessage ? " " : "") + sentence;
}
}
if (currentMessage.trim()) messages.push(currentMessage.trim());
return messages.length > 0 ? messages : [response];
}
async function sendMultipleMessages(messages) {
isMemoTyping = true;
shouldPauseMemo = false;
for (let i = 0; i < messages.length; i++) {
if (shouldPauseMemo) {
removeTyping(chatMessages);
displayMessage(chatMessages, "Pode falar! Estou ouvindo... 😊", "bot");
isMemoTyping = false;
return;
}
const message = messages[i];
const typingTime = Math.max(message.length * 40, 800);
if (i > 0) {
await delay(800 + Math.random() * 600);
if (shouldPauseMemo) {
removeTyping(chatMessages);
displayMessage(chatMessages, "Pode falar! Estou ouvindo... 😊", "bot");
isMemoTyping = false;
return;
}
}
showTyping(chatMessages);
await delay(typingTime);
if (shouldPauseMemo) {
removeTyping(chatMessages);
displayMessage(chatMessages, "Pode falar! Estou ouvindo... 😊", "bot");
isMemoTyping = false;
return;
}
removeTyping(chatMessages);
displayMessage(chatMessages, message, "bot");
}
isMemoTyping = false;
}
setTimeout(() => { showWelcomeMessage(); }, 1000);
sendBtn.addEventListener("click", handleUserMessage);
userInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") { e.preventDefault(); handleUserMessage(); }
});
async function handleUserMessage() {
const userMessage = userInput.value.trim();
if (!userMessage) return;
shouldPauseMemo = true;
removeTyping(chatMessages);
displayMessage(chatMessages, userMessage, "user");
userInput.value = "";
isMemoTyping = false;
shouldPauseMemo = false;
showTyping(chatMessages);
try {
// Gera ou recupera o session_id persistido no navegador
// Garante que o Memo reconhece o usuário mesmo após fechar e reabrir a aba
let sessionId = localStorage.getItem("neurozen_session_id");
if (!sessionId) {
sessionId = "user_" + Math.random().toString(36).substr(2, 9);
localStorage.setItem("neurozen_session_id", sessionId);
}
const response = await fetch(`${API_URL}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId, message: userMessage }),
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
console.log(`Memórias utilizadas: ${data.memories_retrieved}`);
removeTyping(chatMessages);
const messages = splitResponse(data.response);
await sendMultipleMessages(messages);
} catch (error) {
console.error("Erro na API:", error);
removeTyping(chatMessages);
const fallbackResponses = [
"Desculpe, estou com uma instabilidade temporária. Que tal tentar novamente em alguns segundos?",
"Ops! Parece que tive um probleminha técnico. Pode repetir sua pergunta?",
"Nossa, algo deu errado por aqui. Mas fique à vontade para me perguntar sobre o NeuroZen!"
];
displayMessage(chatMessages, fallbackResponses[Math.floor(Math.random() * fallbackResponses.length)], "bot");
}
}
});
📦 Instalação das Dependências
backend/requirements.txt
Entre na pasta com cd backend, e crie o requirements.txt com o conteúdo abaixo.
# Core API
fastapi>=0.115
uvicorn>=0.30
python-dotenv>=1.0.0
# AI & Vector DB
groq>=0.11
pinecone==3.2.2
fastembed>=0.4.0
grpcio-status>=1.48.2
# Framework
langchain>=0.3
langchain-community>=0.3
langchain-groq>=0.2Crie um ambiente virtual e instale tudo:
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
pip install -r requirements.txt🏗️ Arquitetura da Memória
Antes do código, veja o fluxo completo que vamos construir:
- Usuário envia mensagem pelo navegador →
chat.jsenvia para o servidor no Render - Servidor identifica quem é esse usuário pelo
session_id - Pinecone é consultado → retorna os fragmentos mais relevantes da memória desse usuário
- Esses fragmentos + histórico recente + mensagem atual são montados num prompt
- O prompt vai para o Groq → resposta é gerada
- A resposta é enviada ao usuário imediatamente — o salvamento no Pinecone acontece em background
- O Memo “digita” a resposta para o usuário no navegador
Cada usuário tem um session_id único. As memórias são tagueadas com esse ID, então o Memo nunca vai confundir o João com a Maria.
🐍 Arquivos Python
backend/memory_agent.py
Arquivo central do projeto. Contém duas classes:
- AgentMemory — gerencia o Pinecone: salva interações em background, busca memórias relevantes por usuário e retorna perfil do visitante. Usa Lazy Loading para carregar o modelo de embeddings apenas na primeira mensagem, evitando timeout no Render.
- NeuroZenAgent — o agente em si: combina memória de curto prazo (histórico recente no prompt) com memória de longo prazo (Pinecone) e chama o Groq para gerar a resposta.
from groq import Groq
from pinecone import Pinecone, ServerlessSpec
from datetime import datetime
import uuid
import os
from dotenv import load_dotenv
from fastembed import TextEmbedding
# Carrega as variáveis do .env automaticamente (só tem efeito local)
# No Render as variáveis de ambiente são configuradas no painel
load_dotenv()
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
# =============================================================
# CONFIGURAÇÃO
# =============================================================
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
PINECONE_INDEX_NAME = "neurozen-memories"
PINECONE_REGION = "us-east-1"
# Modelo de embeddings — roda via ONNX, sem PyTorch
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
TOP_K_MEMORIES = 3
RECENT_HISTORY_SIZE = 6
AGENT_SYSTEM_PROMPT = """Você é Memo, da equipe do NeuroZen. Fale de forma amigável, natural e levemente descontraída, como um brasileiro simpático explicando um produto que conhece bem.
## LIVRO: NeuroZen - Guia da Mente Criativa com IA
- Autor: Dr. Alexandre Neural
- 280 páginas, PDF + EPUB
- Preço: R$ 97 → HOJE: R$ 47 (promoção limitada)
- 8 capítulos práticos sobre criatividade + IA
## PRINCIPAIS BENEFÍCIOS:
- Aumenta velocidade criativa em 300%
- 15 técnicas para destravar bloqueios
- Funciona com IAs gratuitas (ChatGPT, Claude)
- Técnica dos "5 Cérebros Artificiais"
- 500+ prompts criativos inclusos
- Masterclass 2h + comunidade VIP
- Garantia 30 dias
## COMO VOCÊ DEVE FALAR:
- Use ocasionalmente: "cara", "olha", "nossa"
- Responda em 1-2 mensagens curtas
- Máximo 2-3 frases por mensagem
- NUNCA use asteriscos (*) ou formatação markdown
- Seja entusiasta mas profissional
## INSTRUÇÕES ESPECIAIS DE MEMÓRIA:
{memory_context}
Use as informações acima de forma natural na conversa.
Se souber o nome do usuário, use-o ocasionalmente.
Se souber interesses anteriores, faça referências sutis.
NÃO mencione que tem memória ou que está consultando histórico."""
# =============================================================
# CLASSE DE MEMÓRIA VETORIAL
# =============================================================
class AgentMemory:
"""
Gerencia memória de longo prazo usando Pinecone e embeddings locais.
Cada usuário tem suas memórias isoladas por session_id.
"""
def __init__(self):
print("🧠 Inicializando sistema de memória...")
# Lazy Loading: o modelo só é carregado na primeira mensagem
# Isso evita timeout no Render durante a inicialização do servidor
self.embedding_model = None
self.pc = Pinecone(api_key=PINECONE_API_KEY)
self.index = self.pc.Index(PINECONE_INDEX_NAME)
print(f" ✅ Conectado ao índice '{PINECONE_INDEX_NAME}'.")
print("✅ Sistema de memória pronto (modelo carregado no primeiro uso)!\n")
def _get_model(self):
"""Carrega o modelo de embeddings apenas quando necessário (Lazy Loading)."""
if self.embedding_model is None:
print(f" 📦 Carregando modelo '{EMBEDDING_MODEL}'...")
self.embedding_model = TextEmbedding(model_name=EMBEDDING_MODEL)
print(" ✅ Modelo carregado!")
return self.embedding_model
def _embed(self, text: str) -> list:
"""Converte texto em vetor numérico usando fastembed (ONNX, sem PyTorch)."""
model = self._get_model()
# fastembed retorna um iterador — pegamos o primeiro item
return list(model.embed([text]))[0].tolist()
def save_memory(self, session_id: str, user_message: str, agent_response: str, metadata: dict = None):
"""
Salva uma interação na memória vetorial do Pinecone.
Chamado em background pelo server.py para não bloquear a resposta ao usuário.
"""
memory_text = f"Usuário perguntou: {user_message}\nMemo respondeu: {agent_response}"
memory_metadata = {
"session_id": session_id,
"timestamp": datetime.now().isoformat(),
"user_message": user_message[:200],
"agent_response": agent_response[:200],
"text": memory_text[:500],
}
if metadata:
for k, v in metadata.items():
memory_metadata[k] = str(v)
self.index.upsert(vectors=[{
"id": str(uuid.uuid4()),
"values": self._embed(memory_text),
"metadata": memory_metadata
}])
def retrieve_memories(self, session_id: str, query: str, k: int = TOP_K_MEMORIES) -> list:
"""
Busca os fragmentos de memória mais relevantes para a query atual.
Filtra APENAS as memórias do usuário específico (session_id).
"""
results = self.index.query(
vector=self._embed(query),
top_k=k,
filter={"session_id": {"$eq": session_id}}, # ← filtro crítico por usuário
include_metadata=True
)
memories = []
for match in results["matches"]:
if match["score"] > 0.3:
memories.append({
"text": match["metadata"].get("text", ""),
"metadata": match["metadata"],
"relevance_score": round(match["score"], 2)
})
return memories
def get_user_profile(self, session_id: str) -> dict:
"""Extrai informações de perfil do usuário a partir das memórias salvas."""
results = self.index.query(
vector=[0.0] * 384,
top_k=100,
filter={"session_id": {"$eq": session_id}},
include_metadata=True
)
profile = {
"interaction_count": len(results["matches"]),
"known_name": None,
"first_interaction": None
}
if results["matches"]:
sorted_matches = sorted(
results["matches"],
key=lambda x: x["metadata"].get("timestamp", "")
)
profile["first_interaction"] = sorted_matches[0]["metadata"].get("timestamp")
for match in results["matches"]:
if match["metadata"].get("user_name"):
profile["known_name"] = match["metadata"]["user_name"]
break
return profile
def clear_user_memory(self, session_id: str):
"""Remove todas as memórias de um usuário específico."""
results = self.index.query(
vector=[0.0] * 384,
top_k=1000,
filter={"session_id": {"$eq": session_id}},
include_metadata=False
)
ids = [match["id"] for match in results["matches"]]
if ids:
self.index.delete(ids=ids)
print(f"🗑️ {len(ids)} memórias do usuário {session_id} removidas.")
else:
print(f"ℹ️ Nenhuma memória encontrada para o usuário {session_id}.")
# =============================================================
# CLASSE DO AGENTE COM MEMÓRIA
# =============================================================
class NeuroZenAgent:
"""
Agente de vendas do NeuroZen (Memo) com memória de curto e longo prazo.
"""
def __init__(self):
self.groq_client = Groq(api_key=GROQ_API_KEY)
self.memory = AgentMemory()
# Memória de curto prazo: histórico recente por sessão
# Estrutura: { session_id: [{"role": ..., "content": ...}, ...] }
self.short_term_memory = {}
def _build_memory_context(self, session_id: str, user_message: str) -> str:
"""
Monta o texto de contexto que será injetado no {memory_context} do prompt.
Combina perfil do usuário + memórias relevantes recuperadas do Pinecone.
"""
profile = self.memory.get_user_profile(session_id)
long_term_memories = self.memory.retrieve_memories(session_id, user_message)
context_parts = []
if profile["interaction_count"] > 0:
context_parts.append(
f"INFORMAÇÕES DO USUÁRIO:\n"
f"- Este usuário já teve {profile['interaction_count']} interação(ões) anterior(es).\n"
f"- Primeira interação: {profile.get('first_interaction', 'desconhecida')}"
)
if profile["known_name"]:
context_parts.append(f"- Nome: {profile['known_name']}")
else:
context_parts.append("INFORMAÇÕES DO USUÁRIO:\n- Este é o primeiro contato deste usuário.")
if long_term_memories:
context_parts.append("\nCONTEXTO DE CONVERSAS ANTERIORES (mais relevantes para a pergunta atual):")
for i, mem in enumerate(long_term_memories, 1):
context_parts.append(
f"\n[Memória {i} — relevância: {mem['relevance_score']}]\n{mem['text']}"
)
else:
context_parts.append("\nNão há contexto anterior relevante para esta pergunta.")
return "\n".join(context_parts)
def _get_short_term_history(self, session_id: str) -> list:
if session_id not in self.short_term_memory:
self.short_term_memory[session_id] = []
return self.short_term_memory[session_id]
def _update_short_term_memory(self, session_id: str, role: str, content: str):
"""
Adiciona mensagem ao histórico recente.
Sliding window: mantém apenas as últimas RECENT_HISTORY_SIZE mensagens.
"""
history = self._get_short_term_history(session_id)
history.append({"role": role, "content": content})
if len(history) > RECENT_HISTORY_SIZE:
self.short_term_memory[session_id] = history[-RECENT_HISTORY_SIZE:]
def generate_response(self, session_id: str, user_message: str) -> str:
"""
Gera a resposta do LLM e atualiza a memória de curto prazo.
O salvamento no Pinecone é feito em background pelo server.py.
"""
print(f"\n{'='*50}")
print(f"👤 Usuário [{session_id[:8]}...]: {user_message}")
memory_context = self._build_memory_context(session_id, user_message)
memories_found = self.memory.retrieve_memories(session_id, user_message)
print(f"🧠 Memórias recuperadas: {len(memories_found)}")
system_prompt = AGENT_SYSTEM_PROMPT.format(memory_context=memory_context)
recent_history = self._get_short_term_history(session_id)
messages = [{"role": "system", "content": system_prompt}]
messages.extend(recent_history)
messages.append({"role": "user", "content": user_message})
try:
response = self.groq_client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
max_tokens=500,
temperature=0.7,
)
agent_response = response.choices[0].message.content
except Exception as e:
agent_response = "Desculpe, tive um problema técnico. Pode repetir?"
print(f"❌ Erro Groq: {e}")
print(f"🤖 Memo: {agent_response[:100]}...")
self._update_short_term_memory(session_id, "user", user_message)
self._update_short_term_memory(session_id, "assistant", agent_response)
return agent_response
def chat(self, session_id: str, user_message: str, user_metadata: dict = None) -> str:
"""Versão completa para uso nos scripts de teste locais."""
agent_response = self.generate_response(session_id, user_message)
self.memory.save_memory(
session_id=session_id,
user_message=user_message,
agent_response=agent_response,
metadata=user_metadata or {}
)
return agent_response
backend/test_memory.py
Demonstra dois pontos críticos: que a memória persiste entre sessões mesmo criando uma nova instância do agente, e que as memórias de usuários diferentes não se misturam.
from memory_agent import NeuroZenAgent
import time
def test_persistencia():
print("\n" + "="*60)
print("TESTE 1 — PERSISTÊNCIA DE MEMÓRIA ENTRE SESSÕES")
print("="*60)
agent = NeuroZenAgent()
user_id = "usuario_joao_001"
print("\n📅 SESSÃO 1 — Primeira visita")
print("-"*40)
resposta1 = agent.chat(
session_id=user_id,
user_message="Oi! Meu nome é João. Sou designer e tô com bloqueio criativo.",
user_metadata={"user_name": "João", "profession": "designer"}
)
print(f"Memo: {resposta1}\n")
time.sleep(1)
resposta2 = agent.chat(
session_id=user_id,
user_message="Qual o preço do livro?",
user_metadata={"user_name": "João"}
)
print(f"Memo: {resposta2}\n")
# Nova instância = memória de curto prazo zerada
# Mas Pinecone persiste na nuvem!
print("\n📅 SESSÃO 2 — Usuário retorna (nova instância do agente)")
print("-"*40)
print("⚠️ Nova instância criada — memória de curto prazo zerada\n")
agent2 = NeuroZenAgent()
resposta3 = agent2.chat(
session_id=user_id, # mesmo ID!
user_message="Oi, voltei! Ainda tô pensando no livro.",
)
print(f"Memo: {resposta3}\n")
time.sleep(1)
resposta4 = agent2.chat(
session_id=user_id,
user_message="Você lembra que eu sou designer?",
)
print(f"Memo: {resposta4}\n")
print("✅ O agente deveria ter citado informações da sessão anterior!")
def test_isolamento():
print("\n" + "="*60)
print("TESTE 2 — ISOLAMENTO DE MEMÓRIAS POR USUÁRIO")
print("="*60)
agent = NeuroZenAgent()
agent.chat(
session_id="usuario_ana",
user_message="Sou professora e quero usar IA nas minhas aulas.",
user_metadata={"user_name": "Ana", "profession": "professora"}
)
agent.chat(
session_id="usuario_pedro",
user_message="Sou programador e quero ser mais criativo nos projetos.",
user_metadata={"user_name": "Pedro", "profession": "programador"}
)
print("\n--- Verificando isolamento ---")
resp_ana = agent.chat(
session_id="usuario_ana",
user_message="O livro serve para programadores também?"
)
print(f"Ana (não deve citar Pedro): {resp_ana[:200]}\n")
resp_pedro = agent.chat(
session_id="usuario_pedro",
user_message="Isso serve para professores também?"
)
print(f"Pedro (não deve citar Ana): {resp_pedro[:200]}\n")
print("✅ Teste de isolamento concluído!")
if __name__ == "__main__":
test_persistencia()
# test_isolamento() # descomente para rodar o segundo teste
Execute localmente para testar antes do deploy:
cd backend
python test_memory.pybackend/server.py
Expõe o agente como uma API REST. O salvamento no Pinecone é feito em background via BackgroundTasks do FastAPI — o usuário recebe a resposta imediatamente, sem esperar a gravação da memória.
import os
from fastapi import FastAPI, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from memory_agent import NeuroZenAgent
app = FastAPI(title="NeuroZen Agent API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Uma única instância do agente compartilhada entre todas as requisições
agent = NeuroZenAgent()
class ChatRequest(BaseModel):
session_id: str
message: str
user_name: str | None = None
class ChatResponse(BaseModel):
response: str
memories_retrieved: int
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, background_tasks: BackgroundTasks):
metadata = {}
if request.user_name:
metadata["user_name"] = request.user_name
memories = agent.memory.retrieve_memories(request.session_id, request.message)
# Gera a resposta do LLM
response = agent.generate_response(
session_id=request.session_id,
user_message=request.message
)
# Salva no Pinecone em background — não bloqueia a resposta ao usuário
background_tasks.add_task(
agent.memory.save_memory,
session_id=request.session_id,
user_message=request.message,
agent_response=response,
metadata=metadata if metadata else None
)
return ChatResponse(
response=response,
memories_retrieved=len(memories)
)
@app.get("/profile/{session_id}")
async def get_profile(session_id: str):
"""Retorna o perfil acumulado de um usuário."""
return agent.memory.get_user_profile(session_id)
@app.delete("/memory/{session_id}")
async def clear_memory(session_id: str):
"""Remove todas as memórias de um usuário."""
agent.memory.clear_user_memory(session_id)
return {"message": f"Memórias do usuário {session_id} removidas."}
@app.get("/")
async def root():
"""Health check — o Render usa este endpoint para verificar se o serviço está vivo."""
return {"status": "ok", "agent": "Memo - NeuroZen"}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="0.0.0.0", port=port)
backend/inspect_db.py
Permite visualizar tudo que está salvo no Pinecone — útil para debug e para mostrar ao vivo na aula o que o agente realmente “lembra”.
import os
from dotenv import load_dotenv
from pinecone import Pinecone
load_dotenv()
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
PINECONE_INDEX_NAME = "neurozen-memories"
pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index(PINECONE_INDEX_NAME)
stats = index.describe_index_stats()
print(f"Total de vetores no index: {stats['total_vector_count']}")
results = index.query(
vector=[0.0] * 384,
top_k=50,
filter={"session_id": {"$eq": "usuario_joao_001"}},
include_metadata=True
)
print(f"\nMemórias do usuário 'usuario_joao_001': {len(results['matches'])}")
for match in results["matches"]:
meta = match["metadata"]
print(f"\n📅 {meta.get('timestamp', 'sem data')}")
print(f"📝 {meta.get('text', '')[:150]}...")
🚀 Deploy no Render
Com o código testado localmente, é hora de colocar o servidor na nuvem. O deploy inteiro leva menos de 10 minutos.
1. Suba o projeto para o GitHub
Crie um repositório novo no GitHub e faça o push de toda a pasta do monorepo. Confirme que as chaves de API estão sendo lidas via os.environ.get() — nunca suba chaves no código.
2. Crie o Web Service no Render
- No painel do Render, clique em New → Web Service
- Conecte sua conta do GitHub e selecione o repositório do projeto
- Configure o serviço:
- Name: neurozen-memo-agent
- Language: Python 3
- Branch: main
- Root Directory:
backend← essencial para o monorepo funcionar - Build Command:
pip install -r requirements.txt - Start Command:
uvicorn server:app --host 0.0.0.0 --port $PORT - Instance Type: Free
O Start Command merece atenção: server:app diz ao uvicorn para procurar a instância app dentro do arquivo server.py. O --host 0.0.0.0 permite conexões externas — sem isso o Render não consegue acessar o serviço. O --port $PORT usa a porta que o Render define automaticamente via variável de ambiente.
3. Configure as variáveis de ambiente
Ainda na tela de criação do serviço, desça até Environment Variables e adicione:
GROQ_API_KEY = sua_chave_do_groq
PINECONE_API_KEY = sua_chave_do_pineconeAs chaves nunca sobem para o GitHub — ficam guardadas com segurança no painel do Render.
4. Faça o deploy
Clique em Create Web Service. O Render vai instalar as dependências e iniciar o servidor. Acompanhe os logs em tempo real — quando aparecer Application startup complete, o deploy está completo.
Sua URL pública ficará no formato:
https://neurozen-memo-agent.onrender.comTeste acessando https://neurozen-memo-agent.onrender.com/docs — a documentação interativa do FastAPI deve aparecer.
🌐 Deploy do Frontend no Netlify
Com o backend no Render e o API_URL atualizado no chat.js, é hora de publicar o frontend. O projeto inclui um arquivo netlify.toml na raiz do repositório que instrui o Netlify a publicar apenas a pasta frontend/ e ignorar o backend Python.
netlify.toml
[build]
publish = "frontend"
command = "echo 'Publicando apenas o frontend estático...'"
[context.production.environment]
PYTHON_VERSION = "3.10"- Acesse netlify.com e faça login
- Clique em Add new site → Import an existing project
- Conecte o GitHub e selecione o repositório
neurozen-memo-agent - O Netlify detecta o
netlify.tomlautomaticamente — confirme as configurações e clique em Deploy
O Netlify publica e gera uma URL pública no formato https://nome-aleatorio.netlify.app.
Pronto — o projeto completo está no ar. Backend no Render, frontend no Netlify, memória no Pinecone.
🔍 Entendendo as Partes Mais Importantes
Por que FastEmbed em vez de SentenceTransformers?
O sentence-transformers depende do PyTorch, uma biblioteca pesada que pode causar problemas de compatibilidade em ambientes de deploy. O fastembed usa o formato ONNX — um runtime de inferência muito mais leve, sem dependência do PyTorch — e carrega o mesmo modelo all-MiniLM-L6-v2 com os mesmos vetores de 384 dimensões. O resultado é idêntico, mas o deploy é muito mais simples.
Lazy Loading do modelo
def _get_model(self):
if self.embedding_model is None:
self.embedding_model = TextEmbedding(model_name=EMBEDDING_MODEL)
return self.embedding_modelO modelo de embeddings não é carregado na inicialização do servidor — só na primeira mensagem. Isso evita que o Render encerre o processo por timeout durante o boot, já que o download do modelo (~22MB) pode demorar.
Background Tasks no FastAPI
background_tasks.add_task(
agent.memory.save_memory,
session_id=request.session_id,
...
)O salvamento no Pinecone acontece depois que a resposta já foi enviada ao usuário. Isso remove 1-2 segundos de espera — o usuário recebe a resposta do Memo imediatamente, enquanto a memória é gravada em background.
O filtro por session_id
results = self.index.query(
vector=self._embed(query),
top_k=k,
filter={"session_id": {"$eq": session_id}}, # ← filtro crítico
include_metadata=True
)O parâmetro filter garante que cada usuário só veja suas próprias memórias. Sem isso, João poderia receber fragmentos de memória da Maria — um bug sério em produção.
Sliding window na memória de curto prazo
if len(history) > RECENT_HISTORY_SIZE:
self.short_term_memory[session_id] = history[-RECENT_HISTORY_SIZE:]LLMs têm limite de contexto. Se mandarmos todo o histórico a cada mensagem, eventualmente vamos exceder esse limite. A janela deslizante mantém as mensagens recentes sem explodir o prompt.
A injeção de memória no prompt
system_prompt = AGENT_SYSTEM_PROMPT.format(memory_context=memory_context)O memory_context gerado pode ter essa aparência:
INFORMAÇÕES DO USUÁRIO:
- Este usuário já teve 3 interação(ões) anterior(es).
- Primeira interação: 2025-03-14T10:22:00
- Nome: João
CONTEXTO DE CONVERSAS ANTERIORES:
[Memória 1 — relevância: 0.87]
Usuário perguntou: Sou designer e tô com bloqueio criativo.
Memo respondeu: Cara, o NeuroZen foi feito exatamente pra isso...
[Memória 2 — relevância: 0.71]
Usuário perguntou: Qual o preço do livro?
Memo respondeu: O preço normal é R$ 97, mas hoje tá R$ 47...Com esse contexto no prompt, o Memo sabe com quem está falando sem precisar perguntar tudo de novo.
🧪 Testes para Fazer ao Vivo na Aula
Com o servidor no Render e o frontend publicado no Netlify, faça os testes na ordem — cada um comprova um comportamento diferente do sistema.
Teste 1 — O agente lembra o nome
Abra a URL do Netlify no navegador e envie:
Oi! Meu nome é João e sou designer.Receba a resposta. Agora feche a aba completamente, reabra a URL e envie:
Oi, voltei!O Memo deve cumprimentar pelo nome e mencionar que você é designer — sem você ter dito nada disso na segunda sessão. Se isso acontecer, a memória de longo prazo está funcionando.
O session_id fica salvo no localStorage do navegador e persiste mesmo após fechar e reabrir a aba. Em aba anônima, o localStorage é apagado quando a janela anônima é fechada — simulando um usuário completamente novo.
Teste 2 — A busca é semântica, não por palavras-chave
Na primeira sessão, envie:
Tenho dificuldade para ter ideias novas no trabalho.Feche a aba, reabra e envie algo com palavras completamente diferentes mas mesmo significado:
Minha criatividade tá travada.O Memo deve retomar o contexto da conversa anterior mesmo sem nenhuma palavra em comum. Isso prova que o Pinecone está comparando significado, não texto.
Teste 3 — Memórias de usuários diferentes não se misturam
Na janela normal do navegador, abra a URL do Netlify e envie:
Sou professora e quero usar IA nas minhas aulas.Agora abra o navegador em modo anônimo — isso gera um session_id novo, simulando outro usuário — e envie:
O livro serve para professores?O Memo pode responder que sim, mas não deve mencionar nada sobre a conversa da janela anterior. Para confirmar, abra o console do navegador (F12): o log Memórias utilizadas: X mostra quantos fragmentos foram injetados no prompt de cada sessão.
Teste 4 — Inspecionar o que foi salvo no Pinecone
Rode no terminal:
cd backend
python inspect_db.pyVocê verá todas as interações salvas com timestamp e conteúdo. Você também pode confirmar os dados no painel em app.pinecone.io — isso torna visível o que normalmente seria invisível e ajuda a entender por que cada resposta foi gerada daquela forma.
🎓 Conclusão
Nesta aula você entendeu a diferença entre memória de curto e longo prazo em agentes, como embeddings permitem busca semântica (por significado, não por palavras), como o Pinecone armazena e recupera memórias isoladas por usuário na nuvem, como injetar esse contexto dinamicamente no prompt do LLM, e como publicar o servidor Python gratuitamente no Render com o frontend no Netlify.
O Memo agora é um vendedor com memória real rodando na web — acessível de qualquer lugar pelo link público. Ele lembra o nome do cliente, as dúvidas que já foram feitas, os interesses demonstrados — e usa tudo isso para conversas cada vez mais personalizadas.
Na próxima aula, vamos dar um salto ainda maior: vamos equipar o agente com ferramentas externas. Ele vai poder buscar informações na web, ler arquivos e consultar APIs — decidindo autonomamente qual ferramenta usar em cada situação. Esse é o conceito de Function Calling, e é onde os agentes começam a parecer realmente inteligentes.



