Skip to content

Implementação: Feature Vaivoando (viagensprepagas.com.br)

Visão Geral

Campo Valor
Affiliate ID 11522
Nome Vaivoando (viagensprepagas.com.br)
URLBase https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/{product_id}
Parâmetros ?Data={yyyy-MM-dd}&Hora={HH:mm}&passageiros={AgePolicyId}:{Qtd},...

URL Formatada

https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/{product_id}
?Data={yyyy-MM-dd}
&Hora={HH:mm}
&passageiros={AgePolicyId}:{Quantidade},{AgePolicyId}:{Quantidade}

Exemplo:

https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/1929?Data=2026-06-15&Hora=10:00&passageiros=34:2,35:1

  • 1929 = product_id
  • 34 = AgePolicy "Adulto" (2 pessoas)
  • 35 = AgePolicy "Criança" (1 pessoa)

Arquitetura: Root Agent Específico via AgentBuilder

Integração via SDK JavaScript

O agente Vaivoando será integrado via SDK JavaScript inserido no site da Vaivoando (viagensprepagas.com.br). O SDKenvia o affiliate_id diretamente no metadata da requisição:

// Exemplo de chamada do SDK
sdk.sendMessage("Quero tours em Paris", {
  metadata: {
    affiliate_id: 11522,
    ...outrosCamposOpcionales
  }
});

Fluxo de dados:

Frontend (SDK JS)
    │
    ▼ metadata: { affiliate_id: 11522 }
SSE Adapter (sse_adapter.py)
    │
    ▼ incoming_message.metadata
unified_bot.py
    │
    ▼ session.state.message_metadata = { affiliate_id: 11522 }
AgentBuilder.build(affiliate_id=11522)
    │
    ▼ root_agent específico

Detecção via metadata (não via JWT): - O affiliate_id é recebido no message_metadata da requisição - O AgentBuilder utiliza esse ID para carregar a configuração específica do affiliate - Não há necessidade de parsear o JWT para obter partner_code

Importante: O AgentBuilder.build() deve ser chamado dentro do fluxo de request (não apenas no startup), pois o affiliate_id só está disponível após a primeira mensagem da sessão. Recomenda-se usar um cache em memória para reutilizar o root_agent criado durante a sessão.

Conceito

O AgentBuilder será estendido para detectar o affiliate via metadata e construir o root agent específico para cada affiliate. Isso permite:

  • Prompts menores - cada root agent tem apenas os sub-agentes necessários
  • Fluxo otimizado - sem condicionais, apenas sub-agentes ativos
  • URL transformada automaticamente - parte do fluxo do agente
                    ┌─────────────────────────────────────────┐
                    │        AgentBuilder.build()               │
                    │  1. Recebe affiliate_id via parâmetro     │
                    │  2. Carrega config do affiliate via API  │
                    │  3. Seleciona sub-agents                  │
                    │  4. Gera prompt customizado              │
                    │  5. Retorna root_agent                   │
                    └─────────────────┬───────────────┘
                                  │
        ┌───────────────────────┼───────────────────────┐
        ▼                       ▼                       ▼
┌──────────────┐        ┌──────────────┐        ┌──────────────┐
│Root Genérico│        │Vaivoando  │        │Outro      │
│ (iFriend) │        │ Agent    │        │Affiliate  │
│           │        │(11522)   │        │           │
└─────┬─────┘        └─────┬─────┘        └─────┬─────┘
      │                     │                     │
      ▼                     ▼                     ▼
discovery             discovery              discovery
quote                quote                 quote
booking              +custom_tour           booking
payment              +support              payment
support             +itinerary            support
itinerary                                  itinerary
custom_tour                               custom_tour
...                                     ...

Affiliate Detection no AgentBuilder

# ifriend_agent/agent_builder.py

def build(affiliate_id: int | None = None) -> LlmAgent:
    """
    Constrói root_agent otimizado para o affiliate.

    Args:
        affiliate_id: ID do affiliate (ex: 11522 para Vaivoando).
                   Se None, usa root genérico (iFriend padrão).

    O affiliate_id é passado via parâmetro, vindo do metadata da requisição:
    message_metadata.affiliate_id (enviado pelo SDK JavaScript)
    """
    # 1. Se affiliate_id fornecido, carregar config do affiliate
    if affiliate_id is not None:
        affiliate_config = load_affiliate_config(affiliate_id)

        # FALLBACK: Se API falhar ou não existir config, usar root genérico
        if affiliate_config is None:
            logger.info(f"⚠️ Nenhuma config para affiliate {affiliate_id}, usando root genérico")
            return build_root_generico()

        # 2. Selecionar sub-agents conforme config do affiliate
        sub_agents = build_sub_agents_for_affiliate(affiliate_config)

        # 3. Gerar prompt customizado (custom_greeting, etc.)
        instruction = build_orchestrator_instruction(
            sub_agents,
            affiliate_config
        )

        # 4. Montar root_agent específico do affiliate
        root_agent = LlmAgent(
            name=f"root_agent_{affiliate_id}",
            instruction=instruction,
            sub_agents=sub_agents,
            tools=build_tools_for_affiliate(affiliate_config),
            ...
        )
        logger.info(f"✅ Root agent específico criado para affiliate {affiliate_id}")
        return root_agent

    # 5. Se affiliate_id é None, usar root genérico
    return build_root_generico()


def load_affiliate_config(affiliate_id: int) -> dict | None:
    """
    Carrega configuração do affiliate via API iFriend.

    Autenticação: Usa system auth (X-Context-Partner ou similar)
    Cache: Armazena em memória por sessão (session-level cache)

    Returns:
        None se API falhar ou affiliate não tiver config específica.

    A resposta da API retorna diretamente os campos do root agent (sem campo rootAgent):
    {
        "productUrlTemplate": "https://...",
        "subAgents": ["discovery", "quote", "support"],
        "customGreeting": "Olá! ...",
        "enabled": true,
        "affiliate": "11522",
        "aiAgent": {...}
    }
    """
    # TODO: Implementar com ifriend_api_client usando system auth
    # O endpoint usa o mesmo auth das outras ferramentas (não JWT do usuário)
    try:
        response = ifriend_api_client.get(
            f"/affiliates/{affiliate_id}/root_agent",
            # Auth via system credentials (não JWT do usuário)
        )
        if response.status == 200 and response.data:
            return response.data  # Retorna os dados diretamente
    except Exception as e:
        logger.info(f"⚠️ Erro ao carregar config do affiliate {affiliate_id}: {e}")

    return None  # Fallback para root genérico


def build_root_generico() -> LlmAgent:
    """
    Constrói root_agent genérico (iFriend padrão).

    Usado quando:
    - API /affiliates/{id}/root_agent falha
    - Affiliate não tem config específica
    - affiliate_id é None (sem metadata na requisição)
    """
    # Usa lógica atual com feature flags
    from ifriend_agent.agent_builder import AgentBuilder as OriginalAgentBuilder
    return OriginalAgentBuilder.build()

Comportamento de Fallback

Cenário Comportamento Log
API /affiliates/{id}/root_agent retorna 404 Usa root genérico INFO
API /affiliates/{id}/root_agent retorna erro de rede Usa root genérico INFO
Affiliate não tem config cadastrada Usa root genérico INFO
affiliate_id é None Usa root genérico -
affiliate_id = 11522 (configurado) Usa vaivoando_agent INFO

Cache de Configuração

A configuração do affiliate é carregada uma vez por sessão:

  1. Primeira mensagem da sessão:
  2. O affiliate_id chega via message_metadata (enviado pelo SDK JS)
  3. O AgentBuilder.build(affiliate_id) é chamado para criar root_agent específico
  4. A config é carregada da API /affiliates/{id}/root_agent
  5. O root_agent criado é cacheado em memória (por session_id)

  6. Mensagens subsequentes:

  7. O root_agent específico é reutilizado (sem nova chamada à API)
  8. O affiliate_id continua disponível em session.state.message_metadata

Fluxo simplificado:

Requisição 1 (nova sessão)
  └─ message_metadata.affiliate_id = 11522
     └─ AgentBuilder.build(affiliate_id=11522)
        └─ GET /affiliates/11522/root_agent
        └─ Cria root_agent, cachea por session_id

Requisições 2+ (mesma sessão)
  └─ Usa root_agent cacheado (sem API call)

Fluxo com Fallback

┌─────────────────────────────────────────┐
│      AgentBuilder.build(affiliate_id)    │
│         affiliate_id = 11522            │
└──────────────┬──────────────────────────┘
              │
              ▼
┌──────────────────────────┐
      │ GET /affiliates/11522/root_agent │
      └─────────────┬────────────┘
             │
      ┌─────┴─────┐
      │           │
      ▼           ▼
┌────────┐  ┌────────────────┐
│ 200 OK │  │ 404 / Erro    │
│  config│  │ config = None │
└───┬────┘  └───────┬────────┘
    │               │
    ▼               ▼
┌─────────────┐ ┌──────────────┐
│ Root      │ │ Root        │
│ Específico│ │ Genérico   │
│(vaivoando)│ │ (iFriend)  │
└───────────┘ └─────────────┘

Configuração do Affiliate (via API)

{
  "id": "affiliate_root_123",
  "productUrlTemplate": "https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/{product_id}",
  "subAgents": ["discovery", "quote", "support"],
  "customGreeting": "Olá! Sou o agente da Vaivoando. Como posso ajudar?",
  "enabled": true,
  "affiliate": "11522",
  "aiAgent": {
    "agentKey": "vaivoando_agent"
  }
}

Campos extraídos para construção do agent:

Campo na API Uso no AgentBuilder
aiAgent.agentKey Nome do agent (name)
productUrlTemplate Template para transform_product_url
subAgents Lista de sub-agentes a instanciar
customGreeting Saudação no prompt do orchestrator

Estrutura de Sub-agents por Affiliate

Affiliate sub_agents Ativos
iFriend (padrão) discovery, quote, booking, payment, support, itinerary, custom_tour, ...
Vaivoando (11522) discovery, quote, support
Outro whitelabel discovery, quote, booking, payment, support

Root Agent Vaivoando (Minimizado)

O vaivoando_agent teria apenas:

  • sub_agents: discovery, quote, support (conforme solicitado)
  • tools: transform_product_url, listar_variacoes
  • prompt: ~40 linhas (vs ~60 do root genérico)
┌─────────────────────────────────────────────────────────────┐
│              Affiliate Detection                           │
│  message_metadata.affiliate_id (enviado pelo SDK JS)       │
│  affiliate 11522 = Vaivoando                               │
└───────────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────┐
│         GET /affiliates/{id}/root_agent (via API)           │
│  Response: { productUrlTemplate, subAgents,                │
│             customGreeting, aiAgent, enabled }            │
└───────────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────┐
│              Vaivoando Root Agent                          │
│  sub_agents: discovery, quote, support                │
│  tools: transform_product_url, listar_variacoes        │
│  prompt: customGreeting + instruções específicas         │
└───────────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────┐
│           URL Transformer                                 │
│  product_id + data + hora + age_policies → URL Vaivoando  │
│  Usa productUrlTemplate como base                       │
└─────────────────────────────────────────────────────────────┘

Detecção de Affiliate

O affiliate é detectado através do metadata da requisição (enviado pelo SDK JavaScript):

# No message_metadata (recebido do SDK JS)
session.state.message_metadata.affiliate_id  # 11522

Valor esperado: affiliate_id == 11522

Fonte: - O SDK JavaScript da Vaivoando envia affiliate_id no metadata ao chamar o agente - Não é necessário parsear JWT ou usar partner_code - O metadata é injetado no session.state via ConversationProcessor


Fluxo Conversacional

Fluxo 1:Discovery Agent (busca → seleção → dados → URL)

1. Usuário: "Quero tours em Paris"
2. Agent: Apresenta lista de produtos (URLs theifriend.com)
3. Usuário: "Quero o tour #3"
4. Agent: "Para gerar o link de reserva Vaivoando, preciso de:
   - Data da experiência: ?"
5. Usuário: "15/06/2026"
6. Agent: "Horário: ?"
7. Usuário: "10:00"
8. Agent: "Quantos passageiros por faixa etária?
   - Adultos (18-65): ?"
9. Usuário: "2 adultos, 1 criança"
10. Agent: "Idade da criança?"
11. Usuário: "8 anos"
12. Agent: 
    - Mapeia 2 adultos → AgePolicy Adulto (id: 34)
    - Mapeia 1 criança (8 anos) → AgePolicy Criança (id: 35)
    - Gera URL final
13. Agent: "Aqui está o link para reserva:"
    https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/1929
    ?Data=2026-06-15&Hora=10:00&passageiros=34:2,35:1

Fluxo 2:Quote Agent (após cotação → dados → URL)

1. Usuário: "Quanto custa o tour Paris in Love?"
2. Agent: Busca produto → Cotação → "R$ 500 por pessoa"
3. Agent: "Posso gerar o link de reserva Vaivoando?"
4. Usuário: "Sim"
5. Agent: [Segue fluxo de captura similar ao Discovery]
6. Agent: Gera URL final

Detecção de Affiliate

O affiliate é detectado através do message_metadata da requisição:

# Vem do SDK JavaScript da Vaivoando
session.state.message_metadata.affiliate_id  # 11522

Valor esperado: affiliate_id == 11522

A detecção ocorre no momento de construção do root_agent, onde o AgentBuilderrecebe o affiliate_id como parâmetro. Isso é feito uma vez por sessão (o SDK JS faz uma chamada por sessão).


Requisitos API (iFriend)

Endpoint: GET /affiliates/{affiliate_id}/root_agent

Propósito: Buscar configurações específicas do agente para um affiliate.

Autenticação: Usa system auth (credenciais do backend iFriend, não JWT do usuário).

Request:

GET /affiliates/11522/root_agent
Authorization: Bearer {system_jwt}
# ou
X-Context-Partner: {system_token}

Response:

{
  "id": "affiliate_root_123",
  "productUrlTemplate": "https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/{product_id}",
  "subAgents": ["discovery", "quote", "support"],
  "customGreeting": "Olá! Sou o agente da Vaivoando. Como posso ajudar?",
  "enabled": true,
  "affiliate": "11522",
  "aiAgent": {
    "id": "ai_agent_vaivoando",
    "agentKey": "vaivoando_agent",
    "name": "Agente Vaivoando",
    "serviceUrl": "https://agents.theifriend.com/vaivoando",
    "description": "Agente de atendimento Vaivoando para reservas de tours",
    "createdAt": "2026-05-04T20:08:14.172Z",
    "updatedAt": "2026-05-04T20:08:14.172Z",
    "active": true
  }
}

Campos do Response

Campo Descrição
aiAgent.agentKey Identificador único do agente (usado para construção)
aiAgent.name Nome de exibição do agente
productUrlTemplate Template de URL do produto com placeholder {product_id}
subAgents Lista de sub-agentes ativos (discovery, quote, support, etc.)
customGreeting Saudação customizada apresentada ao usuário
enabled Se o agente está ativo para uso
affiliate ID do affiliate (string)
aiAgent Objeto com dados do AI Agent

Tabelas do Banco de Dados

Essas tabelas já estão criadas e constam nesse documento para referência apenas.

CREATE TABLE `affiliate` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  ...
  `root_agent_id` bigint(20) unsigned DEFAULT NULL,
  ...
  PRIMARY KEY (`id`),
  ...
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `affiliate_aiagent` (
  `affiliate_id` bigint(20) unsigned NOT NULL,
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `product_url_template` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `sub_agents` json DEFAULT NULL,
  `custom_greeting` longtext COLLATE utf8mb4_unicode_ci,
  `enabled` tinyint(1) NOT NULL DEFAULT '1',
  `ai_agent_id` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `affiliate_aiagent_unique` (`affiliate_id`),
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `ai_agent` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `root_agent_id` bigint(20) unsigned DEFAULT NULL,
  `agent_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `service_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `description` longtext COLLATE utf8mb4_unicode_ci,
  ...
  `active` tinyint(1) NOT NULL DEFAULT '1',
  ...
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Campos relevantes para o Vaivoando:

Tabela Campo Uso
affiliate root_agent_id FK para affiliate_aiagent.id
affiliate_aiagent affiliate_id ID do affiliate (11522)
affiliate_aiagent product_url_template Template de URL do produto
affiliate_aiagent sub_agents Lista JSON de sub-agentes
affiliate_aiagent custom_greeting Saudação customizada
affiliate_aiagent enabled Se o agente está ativo
affiliate_aiagent ai_agent_id FK para ai_agent.id
ai_agent agent_key Identificador único do agente
ai_agent active Se o agente está ativo

Implementação no ifriend-agents

1. Nova Tool: get_affiliate_config

# ifriend_agent/tools/get_affiliate_config.py
from google.adk.tools import Tool
from typing import Optional

async def get_affiliate_config(
    tool_context
) -> dict:
    """
    Busca configurações específicas do agente para o affiliate atual.

    A configuração é recuperada do session.state (já carregada pelo AgentBuilder).

    Returns:
        {
            "product_url_template": "https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/{product_id}",
            "enabled": true,
            "sub_agents": ["discovery", "quote", "support"],
            "custom_greeting": "Olá! Sou o agente da Vaivoando..."
        }
        ou
        null se affiliate não tem config específica
    """
    # Busca do session.state (cacheado pelo AgentBuilder)
    affiliate_config = tool_context.state.get("affiliate_config")
    return affiliate_config

get_affiliate_config_tool = Tool(
    name="get_affiliate_config",
    description="Busca configurações do agente para o affiliate atual",
    parameters={}
)

Nota: Esta tool usa a configuração que já foi carregada pelo AgentBuilder e armazenada no session.state. Não faz chamada à API novamente.

2. Nova Tool: transform_product_url

# ifriend_agent/tools/transform_product_url.py
from google.adk.tools import Tool
from typing import List, Optional
from urllib.parse import quote

async def transform_product_url(
    product_id: int,
    data: Optional[str] = None,
    hora: Optional[str] = None,
    age_policies: Optional[List[dict]] = None,
    tool_context
) -> dict:
    """
    Transforma URL do produto para formato do affiliate.

    Args:
        product_id: ID do produto (experience ou guia)
        data: Data no formato yyyy-MM-dd (ex: "2026-06-15")
        hora: Horário no formato HH:mm (ex: "10:00")
        age_policies: Lista de {"id": int, "quantity": int}
            Ex: [{"id": 34, "quantity": 2}, {"id": 35, "quantity": 1}]

    Returns:
        {
            "original_url": "https://theifriend.com/experiences/1929",
            "transformed_url": "https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/1929?Data=2026-06-15&Hora=10:00&passageiros=34:2,35:1",
            "product_id": 1929,
            "affiliate_id": 11522
        }
        ou
        null se affiliate não tem product_url_template configurado
    """
    # 1. Buscar config do session.state (cacheado pelo AgentBuilder)
    affiliate_config = tool_context.state.get("affiliate_config")
    if not affiliate_config or not affiliate_config.get("product_url_template"):
        return None  # Affiliate não tem config de URL

    # 2. Montar query string de passageiros
    passengers = []
    for ap in (age_policies or []):
        if ap.get("id") and ap.get("quantity"):
            passengers.append(f"{ap['id']}:{ap['quantity']}")
    passengers_str = ",".join(passengers) if passengers else ""

    # 3. Montar URL completa
    base_url = affiliate_config["product_url_template"]
    params = []
    if data:
        params.append(f"Data={data}")
    if hora:
        params.append(f"Hora={hora}")
    if passengers_str:
        params.append(f"passageiros={passengers_str}")

    transformed_url = base_url
    if params:
        transformed_url += "?" + "&".join(params)

    return {
        "original_url": f"https://theifriend.com/experiences/{product_id}",
        "transformed_url": transformed_url,
        "product_id": product_id,
        "affiliate_id": affiliate_config.get("affiliate_id")
    }

transform_product_url_tool = Tool(
    name="transform_product_url",
    description="Transforma URL de produto para formato do affiliate",
    parameters={
        "product_id": {"type": "integer", "required": True},
        "data": {"type": "string", "required": False},
        "hora": {"type": "string", "required": False},
        "age_policies": {"type": "array", "items": {"type": "object"}, "required": False}
    }
)

Nota: Esta tool usa o product_url_template que foi carregado pelo AgentBuilder e armazenado no session.state.

3. Update: discovery_agent.py

Adicionar tools: - get_affiliate_config - Para verificar se affiliate tem config de URL - transform_product_url - Para gerar URL formatada - listar_variacoes (para obter agePolicy IDs)

Atualizar prompt:

# No instruction do discovery_agent, adicionar:
"""
## Transformação de URL para Affiliate

Quando o usuário selecionar um produto:
1. Verifique se o affiliate tem product_url_template configurado
   → Use get_affiliate_config() para verificar
2. Se tem config de URL:
   a) Pergunte data, horário e passageiros (faixa etária)
   b) Obtenha as agePolicies via listar_variacoes (da variação selecionada)
   c) Mapeie as quantidades para agePolicy IDs
   d) Use transform_product_url para gerar URL formatada
   e) Apresente a URL ao usuário
3. Se NÃO tem config:
   → Apresente a URL padrão do theifriend.com

A detecção é dinâmica via session.state.affiliate_config (carregado pelo AgentBuilder).
"""

4. Update: quote_agent.py

Adicionar tool: - get_affiliate_config - Para verificar se affiliate tem config de URL - transform_product_url - Para gerar URL formatada

Atualizar prompt:

# No instruction do quote_agent, adicionar:
"""
## Transformação de URL para Affiliate

Após calcular o preço, verifique se o affiliate tem product_url_template configurado:
→ Use get_affiliate_config() para verificar

Se tem config de URL:
1. Pergunte se o usuário deseja gerar link de reserva
2. Se sim, siga o fluxo de captura de data/hora/passageiros
3. Gere URL formatada via transform_product_url
4. Apresente ao usuário

Se NÃO tem config:
→ Não ofereça link de reserva (o fluxo normal continua)

A detecção é dinâmica via session.state.affiliate_config (carregado pelo AgentBuilder).
"""


Mapeamento de Age Policies

Fluxo (discovery/quote agents já fazem)

Os agentes discovery e quote já possuem mecanismo para: 1. Exibir variações disponíveis ao usuário 2. Perguntar qual variação o usuário prefere 3. Obter age policies da variação selecionada

Passo 1: Listar Variações

# via listar_variacoes_tool
{
    "variations": [
        {
            "id": 13,
            "name": "Tour de 6 horas",
            "agePolicies": [
                {"id": 34, "name": "Adulto", "minAge": 18, "maxAge": 65},
                {"id": 35, "name": "Criança", "minAge": 6, "maxAge": 17},
                {"id": 36, "name": "Bebê", "minAge": 0, "maxAge": 5}
            ]
        }
    ]
}

Passo 2: Validar Age Policies

Importante: As age policies usadas devem ser da variação que o usuário selecionou (não de outra variação do mesmo produto). O agente deve validar isso antes de gerar a URL.

Passo 3: Mapear Entrada do Usuário

Entrada Usuário AgePolicy ID Quantidade
2 adultos 34 2
1 criança de 8 anos 35 1
1 bebê 36 1

Passo 4: Gerar Query String

passageiros=34:2,35:1

Nota: Não é necessário feature flag em variável de ambiente — cada affiliate tem sua própria configuração armazenada no banco (affiliate_aiagent) e carregada via API /affiliates/{id}/root_agent.


Checklist de Implementação

API iFriend (Backend)

A API iFriend está já implementada o checklist abaixo relativo a API é para referencia.

  • [ ] Verificar endpoint GET /affiliates/{affiliate_id}/root_agent existente
  • [ ] Garantir que resposta inclua campos: productUrlTemplate, subAgents, customGreeting, aiAgent
  • [ ] Popular registro para affiliate_id=11522 com productUrlTemplate correto
  • [ ] Deploy

ifriend-agents (Frontend Agent)

  • [ ] AgentBuilder: Aceitar affiliate_id como parâmetro
  • [ ] AgentBuilder: Carregar config do affiliate via API (system auth)
  • [ ] AgentBuilder: Cache por sessão (armazenar config no session.state)
  • [ ] AgentBuilder: Selecionar sub-agents conforme config
  • [ ] AgentBuilder: Gerar prompt customizado (custom_greeting)
  • [ ] Criar tool get_affiliate_config.py (global)
  • [ ] Criar tool transform_product_url.py (global)
  • [ ] Atualizar discovery_agent.py (adicionar tools + instrução sobre URL Vaivoando)
  • [ ] Atualizar quote_agent.py (adicionar tool + instrução sobre URL Vaivoando)
  • [ ] Atualizar ConversationProcessor para extrair affiliate_id do metadata
  • [ ] Teste end-to-end com affiliate 11522 (via SDK mock)
  • [ ] Teste com root genérico (sem affiliate_id no metadata)
  • [ ] Teste com outros affiliates (theifriend, whitelabels)
  • [ ] Deploy

Testes

Teste 1:Affiliate Detection via Metadata

def test_detect_vaivoando_affiliate():
    # Simula o que o SDK JavaScript enviaria no metadata
    metadata = {"affiliate_id": 11522}
    session = create_session(message_metadata=metadata)
    assert session.message_metadata.affiliate_id == 11522

Teste 2:URL Transformation

def test_transform_vaivoando_url():
    result = transform_product_url(
        product_id=1929,
        data="2026-06-15",
        hora="10:00",
        age_policies=[
            {"id": 34, "quantity": 2},
            {"id": 35, "quantity": 1}
        ]
    )
    assert result["transformed_url"] == \
        "https://validacao.viagensprepagas.com.br/Vendas/Ifriend/Detalhe/1929?Data=2026-06-15&Hora=10:00&passageiros=34:2,35:1"

Teste 3:Age Policy Mapping

def test_map_age_policies():
    variations = listar_variacoes(1929)
    user_input = {"adultos": 2, "crianca_8anos": 1}

    mapped = map_user_input_to_age_policies(
        user_input, 
        variations[0]["agePolicies"]
    )

    assert mapped == [
        {"id": 34, "quantity": 2},  # Adultos
        {"id": 35, "quantity": 1}   # Criança
    ]

Referências

  • AGENTS.md - Arquitetura Multi-Agent iFriend
  • docs/vaivoando/parametros-agente-vaivoando.md - Requisitos iniciais
  • ifriend_agent/tools/listar_variacoes.py - Age policies
  • ifriend_agent/tools/busca_produtos.py - Busca de produtos
  • ifriend_agent/agents/discovery_agent.py - Agente de descoberta
  • ifriend_agent/agents/quote_agent.py - Agente de cotação