Correção da Serialização de Eventos ADK¶
🎯 Problema Identificado¶
O CloudSQLMemoryService estava perdendo informações críticas ao salvar eventos:
❌ Antes (ERRADO)¶
def _extract_memory_from_session(self, session: Session) -> str:
memories = []
for event in session.events:
if event.author == "model" and event.content:
for part in event.content.parts:
if hasattr(part, "text") and part.text:
memories.append(part.text) # ❌ SÓ TEXTO!
return "\n\n".join(memories) # ❌ STRING SIMPLES
Problemas:
- ❌ Perdia FunctionCall (chamadas de ferramentas)
- ❌ Perdia FunctionResponse (resultados de ferramentas)
- ❌ Perdia estrutura Content/Part do ADK
- ❌ Perdia metadata (author, timestamp, etc)
- ❌ Runner não conseguia reconstruir eventos nativos
Consequência:
Agente chamava busca_produtos_tool → salvava na memória →
memória carregada SEM histórico de tool calls →
agente chamava busca_produtos_tool NOVAMENTE → LOOP INFINITO! 🔄
✅ Solução Implementada¶
1️⃣ Novo Módulo de Serialização (adk_serialization.py)¶
Funções principais:
- serialize_event(event) → Converte Event para Dict JSON
- deserialize_event(dict) → Converte Dict para Event nativo
- serialize_events_to_json(events) → Lista de eventos → JSON string
- deserialize_events_from_json(json_str) → JSON string → Lista de eventos
Preserva:
- ✅ Part.text (texto)
- ✅ Part.function_call (nome, id, args)
- ✅ Part.function_response (nome, id, response)
- ✅ Content (role, parts)
- ✅ Event (author, timestamp, content)
2️⃣ CloudSQLMemoryService Atualizado¶
Schema MySQL (Nova Coluna)¶
CREATE TABLE agent_memories (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
app_name VARCHAR(255) NOT NULL,
user_id VARCHAR(255) NOT NULL,
session_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL, -- ✅ JSON serializado (eventos completos)
text_content TEXT, -- ✅ NOVO: Texto extraído (para busca)
event_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
...
);
Métodos Atualizados¶
_extract_memory_from_session() - Serialização Correta
def _extract_memory_from_session(self, session: Session) -> str:
"""Serializa eventos preservando toda estrutura ADK"""
json_str = serialize_events_to_json(session.events) # ✅ JSON completo
return json_str
_extract_text_for_search() - NOVO
def _extract_text_for_search(self, session: Session) -> str:
"""Extrai texto para indexação/busca"""
texts = []
for event in session.events:
if event.content:
for part in event.content.parts:
if hasattr(part, "text") and part.text:
texts.append(part.text)
return "\n\n".join(texts)
add_session_to_memory() - Salva Ambos
async def add_session_to_memory(self, session: Session):
events_json = self._extract_memory_from_session(session) # JSON completo
text_content = self._extract_text_for_search(session) # Texto para busca
# Salva ambos no MySQL
INSERT INTO agent_memories (..., content, text_content, ...)
VALUES (..., events_json, text_content, ...)
search_memory() - Deserialização Correta
async def search_memory(self, app_name, user_id, query):
# Busca no text_content (LIKE)
SELECT content, text_content FROM agent_memories WHERE text_content LIKE %query%
# Deserializa eventos do JSON
events = deserialize_events_from_json(row['content'])
# Retorna eventos nativos ADK
return SearchMemoryResponse(memories=[
MemoryEntry(content=event.content, author=event.author)
for event in events
])
🚀 Impacto Esperado¶
Antes (com bug)¶
User: "me mostre tênis nike"
Agent: chama busca_produtos_tool("nike tenis") → resultados
Agent: 🔄 chama busca_produtos_tool("nike tenis") NOVAMENTE
Agent: 🔄 chama busca_produtos_tool("nike tenis") NOVAMENTE
... LOOP INFINITO ...
Depois (corrigido)¶
User: "me mostre tênis nike"
Agent: chama busca_produtos_tool("nike tenis") → resultados
Agent: ✅ "Encontrei 10 tênis Nike. Aqui estão as opções..." (responde com texto)
[Runner salva memória com eventos serializados corretamente]
User: "e aquele tênis preto que você mostrou?"
Agent: ✅ Carrega memória → VÊ que já buscou produtos
Agent: ✅ "Você está se referindo ao Nike Air Max Preto por R$ 599?"
📋 Checklist de Deploy¶
- [x] Criar
adk_serialization.py - [x] Atualizar imports em
cloudsql_memory_service.py - [x] Adicionar coluna
text_contentao schema - [x] Atualizar
_extract_memory_from_session()(JSON serializado) - [x] Criar
_extract_text_for_search()(texto para busca) - [x] Atualizar
add_session_to_memory()(salva ambos) - [x] Atualizar
search_memory()(deserializa eventos) - [ ] Deploy no Cloud Run
- [ ] Testar conversa no Slack
- [ ] Verificar logs: "Memória serializada: X eventos, Y bytes"
- [ ] Confirmar: Agent não repete busca de produtos
🔍 Como Verificar¶
1. Checar MySQL após conversa¶
SELECT
session_id,
LENGTH(content) as json_size,
LENGTH(text_content) as text_size,
event_count
FROM agent_memories
ORDER BY created_at DESC
LIMIT 1;
Esperado:
- content deve ter JSON grande (ex: 5000+ bytes)
- text_content deve ter texto extraído (ex: 2000+ bytes)
2. Verificar Logs do Slack Bot¶
✅ Memória serializada: 12 eventos, 4532 bytes
✅ Memória adicionada para U01ABC123 (4532 bytes JSON, 12 eventos)
3. Testar Cenário¶
User: "me mostre tênis adidas"
Bot: [busca e mostra tênis]
User: "e aquele branco?"
Bot: [deve referenciar busca anterior SEM chamar tool novamente]
🛠️ Fallback¶
Se houver erro ao deserializar JSON antigo:
try:
events = deserialize_events_from_json(row['content'])
except:
# Usa text_content como fallback
content = Content(parts=[Part(text=row['text_content'])])
result = MemoryEntry(content=content, author="system")
Dados antigos (pré-correção) serão carregados como texto simples.
📚 Referências¶
- Google ADK Events
- Content/Part/FunctionCall
- Arquivos modificados:
- ifriend_agent/memory/adk_serialization.py (NOVO)
- ifriend_agent/memory/cloudsql_memory_service.py (ATUALIZADO)