Skip to content

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_content ao 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