DEV Community

Cover image for FullAgenticStack WhatsApp-first: Chatbot-as-Code
suissAI
suissAI

Posted on

FullAgenticStack WhatsApp-first: Chatbot-as-Code

Nesse artigo vou demonstrar como pensar em *as-Code e te entregar exatamente os arquivos yaml que você precisará especificar para o seu negócio, toda arquitetura e ferramentas necessárias eu proverei para você apenas usar.

Vamos pegar um chatbot de agendamento de consultas odontológicas para 1 dentista.

ADENDO

O aluno irá receber skills e prompts para fazer a IA retirar as configurações já existentes nos seus sistemas e converter para AllasCode. Ou seja, o aluno não irá aprender a implementar um conceito, irá aprender como funciona e como acontece a extração de cada valor escrito no seu código. Ao final do curso o aluno possuirá um sistema lançado em produção e sem ter nenhum código escrito diretamente, esse é meu framework AllasCode Após 6 meses que eu liberar para os alunos, cada camada, eu liberarei Open Source, porém irei publicar as provas de conceito ao início de cada módulo.

Esse curso, FullAgenticStack, não é para quem gosta de programar, é para quem gosta de criar soluções virtuais para problemas reais.

Para iniciar qualquer processo no nosso sistema precisamos de um canal para receber as mensagens do WhatsApp (Whatsmeow, EvolutionAPI, Whatserl *minha implementação própria baseada no Whatsmeow). Para qualquer sistema que você for criar, você pode começar com a modelagem do banco (schemas) ou com a definição do(s) canal(is) de entrada.

Toda API que eu faço é um Event-driven MCP Server, cada Tool eu crio as interfaces de stdio, websocket, http e filas, foi algo natural para mim, pois todas minhas apis também tinham interface de filas, mantendo o padrão eu criei o MCP-as-Code, que também fará parte do curso.

Para recebermos as mensagens do WhatsApp, precisamos de uma API e uma rota para receber as mensagens. A única rota que precisamos é uma rota POST /webhook.

0. API

Para esse exemplo irei omitir a parte de autenticação e idempotência, escalabilidade, resiliência, EventSourcing, etc. Quando chegarmos mais para frente iremos adicionar mais propriedades para essa configuração.

Qual o ganho de usar o API-as-Code?

Eu vou te dar um exemplo de API-as-Code onde a mesma é executada em: Typescript, Go, Python, Rust e Erlang.

Tá bom?

apiVersion: fas.dev/v1
kind: ApiAsCode
metadata:
  name: whatsapp-webhook-only
spec:
  server:
    port: 8888
    basePath: "/"
  routes:
    - name: whatsapp-webhook
      method: POST
      path: "/webhook"
      input:
        contentType: "application/json"
        schemaRef: "schema.as-code.yaml#/schemas/WhatsAppWebhookPayload"
Enter fullscreen mode Exit fullscreen mode

Esse é apenas um gostinho do poder da Programação declarativa.

Abaixo vou deixar o exemplo funcional de 10 camadas as-Code que você irá aprender seus conceitos e como traduzir seus pensamentos em configurações.

0 programação

1.1) schema.as-code.yaml (contrato mínimo)

apiVersion: fas.dev/v1
kind: SchemaAsCode
metadata:
  name: clinic-schemas
spec:
  schemas:
    WhatsAppWebhookPayload:
      type: object
      properties:
        message:
          type: object
          properties:
            id: { type: string }
            from: { type: string }
            text: { type: string }
            timestamp: { type: string }
          required: [id, from, text, timestamp]
      required: [message]

    WhatsAppMessageReceived:
      type: object
      properties:
        messageId: { type: string }
        from: { type: string }
        text: { type: string }
        timestamp: { type: string }
      required: [messageId, from, text, timestamp]

    Appointment:
      type: object
      properties:
        id: { type: string }
        customerPhoneE164: { type: string }
        startsAt: { type: string, format: date-time }
        durationMinutes: { type: integer }
        status: { type: string }
      required: [id, customerPhoneE164, startsAt, durationMinutes, status]

    BehaviorInputs:
      AppointmentCreateFromTextInput:
        type: object
        properties:
          messageId: { type: string }
          from: { type: string }
          text: { type: string }
          timestamp: { type: string }
        required: [messageId, from, text, timestamp]

    BehaviorOutputs:
      AppointmentCreateOutput:
        type: object
        properties:
          appointmentId: { type: string }
          status: { type: string }
        required: [appointmentId, status]
Enter fullscreen mode Exit fullscreen mode

Faltam (para completar o exemplo):

  • Normalização real do payload (WhatsApp Cloud vs Evolution vs Whatserl)
  • correlation_id / causation_id / idempotency_key no envelope
  • Schema de "erro canônico" (para respostas consistentes)
  • Schema de "evidência/audit" (se for Compliance-evidence-first)

2) channel.as-code.yaml (mínimo do canal)

apiVersion: fas.dev/v1
kind: ChannelAsCode
metadata:
  name: whatsapp-first-clinic
  owner: student
spec:
  queueBackend:
    provider: rabbitmq
    exchange:
      name: "fas.whatsapp"
      type: topic
      durable: true

  naming:
    prefix: "fas"
    environment: "dev"
    rules:
      - pattern: "{prefix}.{env}.{domain}.{entity}.{topic}"
    topics:
      events: "events"
      dlq: "dlq"
      retry: "retry"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • delivery (timeout/retry/backoff)
  • DLQ/retry de verdade (roteamento + políticas por erro)
  • Idempotência no canal (ou no API layer)
  • Observabilidade do canal (métricas, tracing, lag)

3) api.as-code.yaml (webhook → evento canônico)

apiVersion: fas.dev/v1
kind: ApiAsCode
metadata:
  name: whatsapp-webhook-only
spec:
  server:
    port: 8888
    basePath: "/"
  routes:
    - name: whatsapp-webhook
      method: POST
      path: "/webhook"
      input:
        contentType: "application/json"
        schemaRef: "schema.as-code.yaml#/schemas/WhatsAppWebhookPayload"

      emits:
        - topic: "fas.dev.crm.whatsapp.events"
          eventType: "WhatsAppMessageReceived"
          schemaRef: "schema.as-code.yaml#/schemas/WhatsAppMessageReceived"
          map:
            messageId: "{{body.message.id}}"
            from: "{{body.message.from}}"
            text: "{{body.message.text}}"
            timestamp: "{{body.message.timestamp}}"

      response:
        status: 200
        body:
          ok: true
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Autenticação/assinatura (HMAC/verify token)
  • Idempotência (header/body → idempotency key)
  • Rate limit + proteção contra replay
  • Persistência de "raw payload" (controlada por policy/compliance)
  • Modo "respond immediately vs async" (aqui é async por evento)

4) tool.as-code.yaml (1 tool)

Um tool só que encapsula o efeito colateral "criar agendamento + notificar" (o runtime por baixo faz DB + WhatsApp, mas o aluno não mexe nisso).

apiVersion: fas.dev/v1
kind: ToolAsCode
metadata:
  name: clinic-tools
  owner: student
spec:
  tools:
    - name: clinic.appointment.create
      description: "Cria o agendamento (persistência) e notifica o cliente."
      inputSchema:
        type: object
        properties:
          customerPhoneE164: { type: string }
          startsAt: { type: string, format: date-time }
          durationMinutes: { type: integer }
          messageId: { type: string }
        required: [customerPhoneE164, startsAt, durationMinutes, messageId]
      outputSchema:
        type: object
        properties:
          appointmentId: { type: string }
          status: { type: string }
        required: [appointmentId, status]
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Separar efeitos (DB vs WhatsApp) em tools diferentes (quando você quiser granularidade real)
  • Contrato de erro (ex.: "slot indisponível", "fora do horário", "falha no envio")
  • Idempotência no tool (messageId como chave de dedupe)
  • Retries por tipo de erro (transiente vs permanente)

5) policy.as-code.yaml (1 policy)

apiVersion: fas.dev/v1
kind: PolicyAsCode
metadata:
  name: clinic-policy
spec:
  policies:
    Clinic.OfficeHours:
      description: "Agendamento  pode acontecer dentro do horário de funcionamento."
      rules:
        timezone: "America/Sao_Paulo"
        weekly:
          - day: MONDAY
            open: "08:00"
            close: "18:00"
          - day: TUESDAY
            open: "08:00"
            close: "18:00"
          - day: WEDNESDAY
            open: "08:00"
            close: "18:00"
          - day: THURSDAY
            open: "08:00"
            close: "18:00"
          - day: FRIDAY
            open: "08:00"
            close: "18:00"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Feriados/pausas (almoço), exceções e agenda do dentista
  • Política de "slot disponível" (conflito com agenda)
  • Cancelamento/remarcação + janela mínima
  • Política de confirmação contextual (para ações de impacto)

6) compliance.as-code.yaml (1 compliance)

apiVersion: fas.dev/v1
kind: ComplianceAsCode
metadata:
  name: clinic-compliance
spec:
  controls:
    Audit.DecisionLog:
      description: "Registrar evidência mínima de decisão/execução."
      required:
        - eventType
        - correlationId
        - evidence
      sink:
        type: eventStore
        topic: "fas.dev.infra.audit.events"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • PII minimization (o que pode/ não pode ir para audit)
  • Retenção (TTL) e export (LGPD)
  • Trilhas por "ação crítica" (cancelar/pagar)
  • Evidência de "quem/qual agente/qual versão do spec"

7) state.as-code.yaml (estado mínimo)

apiVersion: fas.dev/v1
kind: StateAsCode
metadata:
  name: clinic-state
spec:
  states:
    ConversationState:
      lifecycle:
        - name: "IDLE"
        - name: "AWAITING_DATE"
        - name: "AWAITING_CONFIRMATION"
      defaults:
        state: "IDLE"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Estado por usuário (keyed by from)
  • TTL de sessão (24h WhatsApp window)
  • Migração de estado (State-as-Code migrations)
  • Replay/reprocess de eventos com estado materializado

8) entity-agent.as-code.yaml (1 agent)

Um agente só: ClinicAgent (ele "entende" agendamento; Customer vira detalhe implícito pelo telefone).

apiVersion: fas.dev/v1
kind: EntityAgentAsCode
metadata:
  name: clinic-agent
spec:
  agents:
    ClinicAgent:
      entity: Appointment
      schemaRef: "schema.as-code.yaml#/schemas/Appointment"
      stateRef: "state.as-code.yaml#/states/ConversationState"
      tools:
        - "tool.as-code.yaml#/tools/clinic.appointment.create"
      policies:
        - "policy.as-code.yaml#/policies/Clinic.OfficeHours"
      compliance:
        - "compliance.as-code.yaml#/controls/Audit.DecisionLog"
      behaviors:
        - "atomic-behavior-agent.as-code.yaml#/agents/Appointment.CreateFromText"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Invariants-as-Code (futuro, idempotência, confirmação)
  • Tool permissions (allow/deny por ambiente)
  • Observabilidade por agente (last decision, last error, etc)
  • Multi-intent (cancelar, remarcar, perguntar preço, etc)

9) atomic-behavior-agent.as-code.yaml (1 behavior)

Aqui eu mantive "steps" bem curtos. Repara que eu uso run: Extract como capacidade do runtime (não conta como tool). O único tool é o clinic.appointment.create.

apiVersion: fas.dev/v1
kind: AtomicBehaviorAgentAsCode
metadata:
  name: clinic-behaviors
spec:
  agents:
    Appointment.CreateFromText:
      description: "Extrai data/hora do texto e cria agendamento."
      inputSchemaRef: "schema.as-code.yaml#/schemas/BehaviorInputs/AppointmentCreateFromTextInput"
      outputSchemaRef: "schema.as-code.yaml#/schemas/BehaviorOutputs/AppointmentCreateOutput"

      steps:
        - run: Extract
          description: "Extrai startsAt e durationMinutes do texto."
          from:
            text: "{{input.text}}"
          into:
            startsAt: "{{extract.datetime}}"
            durationMinutes: "{{extract.int | default(30)}}"

        - run: PolicyCheck
          ref: "policy.as-code.yaml#/policies/Clinic.OfficeHours"
          with:
            datetime: "{{startsAt}}"

        - run: Tool
          ref: "tool.as-code.yaml#/tools/clinic.appointment.create"
          with:
            customerPhoneE164: "{{input.from}}"
            startsAt: "{{startsAt}}"
            durationMinutes: "{{durationMinutes}}"
            messageId: "{{input.messageId}}"
          saveAs: result

        - run: Compliance
          ref: "compliance.as-code.yaml#/controls/Audit.DecisionLog"
          with:
            eventType: "AppointmentCreated"
            correlationId: "{{input.messageId}}"
            evidence:
              from: "{{input.from}}"
              startsAt: "{{startsAt}}"
              appointmentId: "{{result.appointmentId}}"

      returns:
        appointmentId: "{{result.appointmentId}}"
        status: "{{result.status}}"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Specification-as-Code (perguntas de clarificação quando startsAt não vem)
  • Invariants-as-Code (agendamento no futuro, idempotência, confirmação)
  • Tratamento de erro (slot ocupado, fora do horário, ambíguo)
  • "Confirmation step" (antes de criar de fato) — ação de impacto

10) flow.as-code.yaml (1 flow)

Um flow único que roteia o evento de entrada para o agente.

apiVersion: fas.dev/v1
kind: FlowAsCode
metadata:
  name: whatsapp-inbound-routing
spec:
  triggers:
    - name: on-whatsapp-message
      type: event
      topic: "fas.dev.crm.whatsapp.events"
      eventType: "WhatsAppMessageReceived"

      action:
        agentRef: "entity-agent.as-code.yaml#/agents/ClinicAgent"
        behaviorRef: "atomic-behavior-agent.as-code.yaml#/agents/Appointment.CreateFromText"
        inputMap:
          messageId: "{{event.messageId}}"
          from: "{{event.from}}"
          text: "{{event.text}}"
          timestamp: "{{event.timestamp}}"
Enter fullscreen mode Exit fullscreen mode

Faltam:

  • Retry/DLQ no nível do flow (transiente vs permanente)
  • Idempotência do flow (dedupe por messageId)
  • Roteamento por intenção (create vs cancel vs info)
  • "Flow-as-Code cron" (ex.: lembrete 24h) — você pediu 1 flow, então ficou fora

Além dessas 10, você só precisará de mais 2 para ter um chatbot completo:

11. Intent-as-Code

12. Specification-as-Code

CTA

Minha arquitetura ainda possui muitas outras especificações as-Code, mas eu quis deixar apenas o necessário para o funcionamento do chatbot.

Top comments (0)