Design: Integração do Proposal Agent no TRIP¶
Data: 2026-05-26 Status: Aprovado para implementação Projeto principal: ifriend-agents Projeto secundário: ifriend-nextjs
Objetivo¶
Absorver o Proposal Agent (protótipo em Next.js) diretamente no agente TRIP como um sub-agente nativo, eliminando a dependência da rota /api/proposal-generator/generate. O usuário poderá solicitar um orçamento em PDF via chat e o TRIP conduzirá o processo — coletando dados conversacionalmente ou a partir de um arquivo enviado — e retornará um bloco visual que o frontend renderiza com o componente ProposalPreview existente.
Arquitetura¶
┌─────────────────────────────────────────────────────────────┐
│ Next.js (ifriend-nextjs) │
│ │
│ Chat UI ──── useAgentChat ──── POST /sse/chat │
│ ↑ { message, files?: [...] } │
│ │ │
│ BlockRenderers │
│ └── proposal_data block │
│ └── <ProposalPreview> (componente existente) │
│ └── Exportar PDF │
└─────────────────────────────────────────────────────────────┘
│ SSE stream
┌─────────────────────────────────────────────────────────────┐
│ TRIP Backend (ifriend-agents) │
│ │
│ SSE Adapter │
│ └── aceita files[] → Part.from_bytes() multimodal │
│ inclui como partes extras do user_content │
│ │
│ root_agent (orquestrador) │
│ └── detecta "proposta / orçamento PDF" │
│ └── transfer → proposal_agent │
│ │ │
│ ├── Fase 1: coleta via entrevista │
│ │ OU processa arquivo do session.state │
│ │ │
│ └── Fase 2: chama gerar_proposta_tool │
│ │ │
│ ├── Vertex AI gemini-2.5-flash │
│ ├── system prompt migrado │
│ ├── ProposalData schema │
│ └── retorna proposal_data block │
└─────────────────────────────────────────────────────────────┘
Backend — ifriend-agents¶
Novos arquivos¶
ifriend_agent/tools/gerar_proposta_tool.py¶
Tool principal da geração. Responsabilidades:
- Receber dados_coletados (str) com os dados da entrevista já consolidados
- Ler session.state["proposal_files"] se existirem arquivos anexados
- Construir o Content multimodal (partes de arquivo + texto) para o Vertex AI
- Chamar gemini-2.5-flash via Vertex AI com:
- system_instruction migrado do protótipo Next.js (brand rules + grounding rules)
- responseMimeType = "application/json"
- responseSchema = PROPOSAL_SCHEMA (campos de ProposalData)
- temperature = 0.3 (mais preciso para extração de dados)
- Retornar no padrão __ui_blocks__:
{
"__ui_blocks__": True,
"blocks": [{"type": "proposal_data", "data": { ...ProposalData... }}]
}
Schema ProposalData (migrado do protótipo):
- partnerName, partnerSubtitle, destination, country
- travelMonth, eventName, period, participants
- totalEur, totalBrl, exchangeRate, pricePerPersonEur, pricePerPersonBrl
- sections[] → sectionTitle + cards[] (emoji, title, pax, pricePerPerson, totalPrice, description, structureLine, subTitle, bulletItems[], italicItems[])
- returnDayText, pendingItems[], observations[]
- paymentEntry, paymentEntryDesc, paymentBalance, paymentBalanceDesc
- footerLine1, footerLine2, missing_fields[]
ifriend_agent/agents/proposal_agent.py¶
Sub-agente conversacional. Segue o padrão LlmAgent dos demais sub-agentes (ex: quote_agent).
Instrução em 2 fases:
Fase 1 — Coleta de dados:
Se o usuário enviou arquivo(s): processar diretamente sem perguntas, extrair dados automaticamente.
Se não enviou arquivo, conduzir entrevista sequencial: 1. Nome da empresa/parceiro e subtítulo 2. Destino + país 3. Mês de viagem + período exato (datas) 4. Nome do evento/grupo 5. Número de participantes 6. Serviços incluídos por seção (ex: "Dia 1 — Transfer + Jantar") 7. Preços por item (por pessoa e total) 8. Condições de pagamento (entrada e saldo) 9. Observações livres
Regras da entrevista:
- Uma pergunta por vez
- Aceitar respostas parciais e completar com missing_fields
- Confirmar dados antes de gerar
Fase 2 — Geração:
- Consolidar dados em JSON estruturado
- Chamar gerar_proposta_tool(dados_coletados=<json_consolidado>)
- Após retorno do bloco, informar ao usuário e transferir ao root_agent
Tools do proposal_agent:
- gerar_proposta_tool
Config:
proposal_agent = LlmAgent(
name="proposal_agent",
model=MODEL, # gemini-2.5-flash via Vertex AI
generate_content_config=build_generate_content_config(temperature=0.7),
description="Gera proposta comercial em PDF para grupos e eventos. ...",
instruction=PROPOSAL_AGENT_INSTRUCTION,
tools=[gerar_proposta],
disallow_transfer_to_peers=True,
after_model_callback=save_last_response_to_state,
)
Arquivos modificados¶
| Arquivo | Mudança |
|---|---|
ifriend_agent/agents/__init__.py |
Exporta proposal_agent |
ifriend_agent/agent_builder.py |
Adiciona "ENABLE_PROPOSAL": proposal_agent em _FLAG_TO_AGENT |
ifriend_agent/config/feature_flags.py |
Adiciona ENABLE_PROPOSAL com default true |
ifriend_agent/prompts/orchestrator_prompt.py |
Routing hint + disabled hint |
messaging/adapters/sse_adapter.py |
Aceita campo files[] no body do POST /sse/chat; converte cada item {name, mimeType, data} para types.Part.from_bytes(); inclui as partes diretamente no user_content (ao lado do texto), sem passar por session.state |
messaging/sse/stream_manager.py |
Campo files: list opcional no dataclass Stream |
Routing hint (orchestrator_prompt.py)¶
"proposal_agent": (
"→ transfer_to_proposal_agent: gerar proposta comercial, "
"orçamento em PDF, proposta de viagem para grupo ou evento, "
"montar proposta com serviços e preços para cliente"
),
Disabled hint (orchestrator_prompt.py)¶
"proposal_agent": (
"⚠️ Geração de propostas comerciais NÃO está habilitada.\n"
"Se o usuário pedir uma proposta ou orçamento em PDF, informe "
"educadamente que no momento não está disponível via chat. "
"Sugira falar com um consultor via support_agent."
),
Feature flag¶
ENABLE_PROPOSAL=true # habilitado por padrão
SSE adapter — extensão de arquivo¶
Os arquivos são incluídos diretamente como partes adicionais do user_content (padrão multimodal do ADK), sem passar por session.state (que não aceita objetos não-serializáveis como types.Part).
# _process_stream — ao montar user_content:
files_raw = incoming_message.metadata.get("proposal_files_raw", [])
file_parts = []
for f in files_raw:
if f.get("data") and f.get("mimeType"):
raw_bytes = base64.b64decode(f["data"])
file_parts.append(types.Part.from_bytes(data=raw_bytes, mime_type=f["mimeType"]))
user_content = types.Content(
role="user",
parts=[types.Part.from_text(text=incoming_message.text)] + file_parts
)
Em parse_webhook_request, os arquivos raw ({name, mimeType, data}) são salvos em metadata["proposal_files_raw"] para serem convertidos em _process_stream.
Frontend — ifriend-nextjs¶
Novos tipos (src/types/aiAgent.ts)¶
export interface ProposalDataBlock {
type: 'proposal_data'
data: ProposalData // importado de @/types/proposal-generator
}
// Adicionar ao union type Block:
export type Block = TextBlock | WhatsappFormBlock | ExperienceCardsBlock
| IfriendCardsBlock | OptionsBlock | ExperienceResultsBlock
| IfriendResultsBlock | ProposalDataBlock // ← novo
ProposalPreview — nova prop embedded¶
ProposalPreview recebe uma nova prop opcional embedded?: boolean. Quando true, o botão "Voltar" (que chama onBack) é ocultado — não faz sentido dentro do chat. A prop onBack permanece obrigatória para compatibilidade com a página existente.
interface ProposalPreviewProps {
data: ProposalData
onBack: () => void
embedded?: boolean // ← novo: oculta o botão Voltar
}
BlockRenderers.tsx — novo handler¶
if (block.type === 'proposal_data') {
const b = block as ProposalDataBlock
return (
<div key={i} className="w-full">
<ProposalPreview data={b.data} onBack={() => {}} embedded />
</div>
)
}
ProposalPreview já tem o botão de exportar PDF — será exibido diretamente no chat sem precisar navegar para /proposal-generator.
useAgentChat.ts — suporte a arquivos¶
sendMessage recebe parâmetro opcional files:
const sendMessage = useCallback(async (text: string, files?: UploadedFile[]) => {
// ...
body: JSON.stringify({
message: userMessage.content,
files: files ?? [], // ← novo campo
// ... demais campos
})
})
ChatInterface.tsx — botão de anexo¶
Adicionar ícone de clipe (paperclip) ao lado do input. Ao clicar, abre file picker (accept=".pdf,.csv,.txt,.png,.jpg,.jpeg"). Arquivos selecionados são base64-encodados (mesmo padrão do protótipo) e passados para sendMessage.
Rota removida¶
src/app/api/proposal-generator/generate/route.ts pode ser removida após validação em produção.
A página /proposal-generator pode ser mantida temporariamente como fallback ou removida junto.
Fluxo completo de dados¶
1. Usuário: "quero gerar uma proposta para grupo em Paris"
2. TRIP orquestrador → transfer_to_proposal_agent
3. proposal_agent: "Claro! Qual o nome da empresa/parceiro para a proposta?"
4. [entrevista: 7-9 perguntas, uma por vez]
OU
[usuário envia arquivo → SSE adapter converte → parte extra do user_content]
5. proposal_agent consolida dados → chama gerar_proposta_tool(dados_coletados)
6. gerar_proposta_tool:
- Recebe dados consolidados da entrevista (e/ou arquivos já no contexto do ADK)
- Chama Vertex AI com system_prompt + schema + dados + arquivos
- Recebe ProposalData JSON
- Retorna {"__ui_blocks__": True, "blocks": [{"type": "proposal_data", "data": {...}}]}
7. SSE adapter emite evento "blocks" para o frontend
8. BlockRenderers renderiza <ProposalPreview data={...} />
9. Usuário clica "Exportar PDF" → download
10. proposal_agent informa sucesso → transfer de volta ao root_agent
Tratamento de erros¶
| Cenário | Comportamento |
|---|---|
| Dados insuficientes para gerar | gerar_proposta_tool preenche campos ausentes com null e lista em missing_fields; proposal_agent informa o usuário |
| Vertex AI retorna JSON inválido | Tool retorna mensagem de erro via texto; proposal_agent sugere tentar novamente |
| Arquivo com formato não suportado | SSE adapter ignora o arquivo; proposal_agent conduz entrevista normalmente |
ENABLE_PROPOSAL=false |
Orquestrador usa disabled hint e redireciona ao support_agent |
O que NÃO muda¶
- Fluxo SSE existente — apenas estende o body do POST
- Todos os outros sub-agentes — nenhuma alteração
ProposalPreviewe lógica de PDF no frontend — reutilizados sem mudança- Autenticação JWT — sem alteração
- Session e memory services — sem alteração
Arquivos a remover após validação¶
ifriend-nextjs/src/app/api/proposal-generator/generate/route.tsifriend-nextjs/src/app/[locale]/proposal-generator/(página completa, opcional)ifriend-nextjs/src/components/ProposalGenerator/InputPanel.tsx(substituída pelo chat)