Skip to content

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
  • ProposalPreview e 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.ts
  • ifriend-nextjs/src/app/[locale]/proposal-generator/ (página completa, opcional)
  • ifriend-nextjs/src/components/ProposalGenerator/InputPanel.tsx (substituída pelo chat)