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_id34= 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:
- Primeira mensagem da sessão:
- O
affiliate_idchega viamessage_metadata(enviado pelo SDK JS) - O AgentBuilder.build(affiliate_id) é chamado para criar root_agent específico
- A config é carregada da API
/affiliates/{id}/root_agent -
O root_agent criado é cacheado em memória (por session_id)
-
Mensagens subsequentes:
- O root_agent específico é reutilizado (sem nova chamada à API)
- O
affiliate_idcontinua disponível emsession.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