Skip to main content
Glama
raiffhigor

Harpia Omnichannel MCP

by raiffhigor
README.md
# Harpia Omnichannel MCP - versão produção

Projeto Node.js com núcleo desacoplado do canal de entrada, preparado para onion architecture e adapters plugáveis de canal, banco e LLM.

## O que já vem pronto

- Núcleo de aplicação independente do canal
- Webhook de verificação da Meta
- Reconhecimento de fala (audio recebido no WhatsApp)
- Resposta por voz (audio enviado no WhatsApp, com fallback para texto)
- Entrada HTTP genérica para qualquer origem
- Validação da assinatura `X-Hub-Signature-256`
- Estrutura multi-tenant
- Repositório local em arquivo JSON para começar rápido
- Adapter de dados mock para WMS/MCP
- Adapter de dados PostgreSQL
- Adapter de dados Oracle (edge-agent)
- Adapter de dados via edge-agent remoto por tenant
- Adapter de LLM compatível com OpenAI
- Dockerfile e docker-compose
- Logs simples em arquivo para rastreabilidade

## Estrutura

```txt
packages/
  core-server/
    src/
      app.js
      server.js
  edge-agent/
    src/
      server.js
  shared/
    src/
      edge/
        operations.js
src/
  application/
  controllers/
  domain/
  infrastructure/
  interfaces/
  middlewares/
  repositories/
  routes/
  services/
  utils/
  app.js
  server.js
  edgeAgentServer.js
data/
  mcp-tools/
    BaseToolDefinition.js
    definitions/
```

Os arquivos em `src/app.js`, `src/server.js` e `src/edgeAgentServer.js` foram mantidos como shims de compatibilidade. Os runtimes distribuíveis agora vivem em `packages/core-server` e `packages/edge-agent`, com contrato comum em `packages/shared`.

## Instalação

```bash
npm install
copy .env.example .env
npm start
```

Runtimes disponíveis:

```bash
npm run start:core
npm run start:edge-agent
```

## Base de testes local

Para subir uma base local sem depender de tenant real, usuários reais ou credenciais reais:

```bash
npm run bootstrap:test-data
```

Isso popula o PostgreSQL local com a fixture em `data/test-bootstrap.json`, incluindo:
- tenant `tenant-teste-local`
- usuários OAuth `claude.teste`, `chatgpt.teste` e `admin.teste`
- clients OAuth `claude-local` e `chatgpt-local`
- perfis `user_access` para uso via MCP/HTTP
- credencial MCP `claude-mcp-local`

Senhas padrão da fixture:
- `claude.teste` -> `claude123`
- `chatgpt.teste` -> `chatgpt123`
- `admin.teste` -> `admin123`

Se quiser reaplicar automaticamente esses dados a cada startup local do core:

```env
TEST_DATA_BOOTSTRAP_ENABLED=true
TEST_DATA_FIXTURE_FILE=./data/test-bootstrap.json
DATA_BACKEND=mock
```

Observação:
- A fixture é pensada para ambiente local e usa backend `mock` no tenant de teste.
- O bootstrap é idempotente: faz insert/update sem depender de base real pré-populada.

Modo desenvolvimento (hot-reload):

```bash
npm run dev
npm run dev:edge-agent
```

## Desenvolvimento com Docker (sem rebuild a cada alteracao)

Use o compose de desenvolvimento para montar o codigo-fonte no container e aplicar alteracoes com hot-reload.

Subir ambiente dev:

```bash
docker compose -f docker-compose.dev.yml up -d --build
```

Ver logs:

```bash
docker compose -f docker-compose.dev.yml logs -f --tail=100
```

Parar ambiente dev:

```bash
docker compose -f docker-compose.dev.yml down
```

Observacoes:
- O build inicial ainda e necessario para instalar dependencias.
- Depois disso, alteracoes de codigo sao aplicadas sem rebuild da imagem.
- O comando usado no container e `npm run dev` (`node --watch`).

Desenvolvimento do edge-agent sem rebuild:

```bash
docker compose -f packages/edge-agent/docker-compose.dev.yml up -d --build
docker compose -f packages/edge-agent/docker-compose.dev.yml logs -f --tail=100
docker compose -f packages/edge-agent/docker-compose.dev.yml down
```

## Atualizacao rapida na VPS (producao)

No ambiente de producao (sem bind mount), o fluxo mais rapido e:

```bash
cd /caminho/do/projeto
git pull
docker compose up -d --build --no-deps harpia-whatsapp-mcp
docker compose ps
docker compose logs --tail=80 harpia-whatsapp-mcp
```

Esse comando reconstrói e reinicia somente o servico do core, sem derrubar dependencias desnecessariamente.

## Deploy no Portainer (Server)

Arquivos prontos para stack:
- `docker-compose.portainer.yml`
- `.env.portainer.example`

Passos:
1. Copie `.env.portainer.example` para `.env` e preencha tokens/chaves.
2. No Portainer, crie uma Stack usando o conteúdo de `docker-compose.portainer.yml`.
3. Garanta que a stack tenha acesso ao diretório `data/` (bind `./data:/app/data`).
4. Faça deploy e valide em `GET /health`.

Observação:
- Este compose é para o `core-server` no servidor.
- O `edge-agent` roda no cliente e se conecta ao core via `edge/poll` e `edge/result`.

Para gerar artefatos standalone de deploy:

```bash
npm run export:runtimes
```

O comando gera:
- `dist/core-server`: bundle para instalar no seu servidor
- `dist/edge-agent`: bundle para instalar no cliente

## Arquitetura

- `domain`: regras puras de linguagem e intenção.
- `application`: casos de uso independentes de canal.
- `infrastructure`: adapters de banco, LLM, logging e canais de saída.
- `interfaces`: adapters de entrada, como HTTP e webhook da Meta.
- `packages/core-server`: runtime do servidor central.
- `packages/edge-agent`: runtime local instalado no cliente.
- `packages/shared`: contratos compartilhados entre core e agente.

O webhook do WhatsApp agora é apenas um adapter. O mesmo caso de uso pode ser acionado por HTTP, CLI, fila, e-mail ou outro canal futuro.

As operacoes usadas pelas tools sao carregadas a partir das ToolDefinitions persistentes em `data/mcp-tools/definitions`
e refletidas no runtime sem depender de `data/tools.json`.

O provider usado pelo core continua sendo definido globalmente por `DATA_BACKEND`.

## Cadastro de tenant

Os tenants ficam persistidos no PostgreSQL, na tabela `tenants`, e podem ser gerenciados pelo painel admin.

Exemplo:
```json
[
  {
    "tenantId": "cliente-demo",
    "name": "Cliente Demo",
    "phoneNumberId": "123456789012345",
    "businessAccountId": "987654321",
    "accessToken": "EAAB...",
    "verifyToken": "opcional_por_tenant",
    "edgeAgent": {
      "mode": "reverse-client",
      "token": "token-compartilhado"
    },
    "orchestration": {
      "mode": "llm-mcp"
    },
    "llm": {
      "provider": "openai-compatible",
      "baseUrl": "https://api.openai.com/v1",
      "apiKey": "sk-...",
      "model": "gpt-4o-mini",
      "assistantName": "Ariane WMS",
      "assistantIdentity": "Eu sou a Ariane WMS, assistente operacional oficial da Cliente Demo.",
      "systemPrompt": "Sempre se identifique usando a mensagem oficial da empresa quando perguntarem sobre sua identidade.",
      "audio": {
        "provider": "elevenlabs",
        "baseUrl": "https://api.elevenlabs.io/v1",
        "apiKey": "elevenlabs-api-key",
        "voiceId": "JBFqnCBsd6RMkjVDRZzb",
        "modelId": "eleven_multilingual_v2",
        "outputFormat": "mp3_44100_128",
        "stability": 0.45,
        "similarityBoost": 0.8,
        "style": 0,
        "speakerBoost": true,
        "sttProvider": "openai-compatible",
        "sttBaseUrl": "https://api.openai.com/v1",
        "sttApiKey": "sk-...",
        "sttModel": "whisper-1"
      },
      "mcpOrchestratorEnabled": true,
      "mcpOrchestratorMaxToolRounds": 3
    },
    "wms": {
      "type": "mock",
      "oracleConnectString": "",
      "oracleUser": "",
      "oraclePassword": ""
    },
    "active": true
  }
]
```

## Identidade e permissões de usuário

Para controlar quais usuários podem ver quais dados, use a tabela `user_access` no PostgreSQL.

Chave de identificação do usuário:
- `tenantId` + `channel` + `userId`

Exemplos de `userId` por canal:
- `whatsapp`: número/wa id (ex.: `5511999999999`)
- `http`: id interno do usuário
- `cli`: id técnico de operador

Campos principais do perfil:
- `scopes`: lista de escopos permitidos (`receiving_status`, `stock_summary`, `picking_status`)
- `maxInfoLevel`: nível máximo de informação permitido (numérico)
- `branchIds`: lista de filiais permitidas para o usuário
- Formato recomendado: `[{ "code": "SP01", "description": "Sao Paulo" }]`
- Compatibilidade: também aceita array de string (`["SP01", "RJ02"]`)
- `active`: habilita/desabilita o perfil

Variáveis:
- `USER_BRANCH_CONTEXT_TTL_MS` (tempo para manter a filial selecionada na sessão do usuário)

O core valida o usuário antes de responder e bloqueia acessos sem perfil/permissão.

Fluxo para usuário com múltiplas filiais:
1. Se o usuário tiver várias `branchIds`, o bot pede seleção da filial.
2. O usuário responde com o código da filial (ex: `SP01`).
3. O core mantém essa filial em contexto temporário por usuário/canal.
4. Consultas seguintes usam essa filial até expirar o contexto.

## URLs

- Runtime dos adapters:
  - `GET /api/runtime`
- Entrada genérica:
  - `POST /api/interactions`
- Verificação Meta:
  - `GET /meta/webhook`
- Recebimento de mensagens:
  - `POST /meta/webhook`
- Health:
  - `GET /health`
- MCP JSON-RPC:
  - `POST /mcp`

## Fluxo

1. Um canal de entrada envia a interação para o caso de uso.
2. O caso de uso resolve tenant, intenção e provider.
3. O adapter de dados consulta o backend configurado.
4. O adapter de LLM pode complementar respostas livres.
5. O adapter de saída devolve a resposta ao canal.

## Exemplo de entrada genérica

```bash
curl -X POST http://localhost:3000/api/interactions \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "cliente-demo",
    "channel": "http",
    "userId": "usuario-demo",
    "userName": "Operador",
    "text": "estoque"
  }'
```

## Configuração de providers

- `DATA_BACKEND=mock`: usa o adapter mock atual.
- `DATA_BACKEND=postgres`: usa adapter PostgreSQL via `pg`.
- `DATA_BACKEND=oracle`: usa adapter Oracle via `oracledb`.
- `DATA_BACKEND=edge-agent`: cloud core consulta um edge-agent no cliente.
- `LLM_PROVIDER=disabled`: desliga LLM.
- `LLM_PROVIDER=openai-compatible`: usa qualquer endpoint compatível com OpenAI.
- `LLM_BASE_URL`, `LLM_API_KEY` e `LLM_MODEL`: definem o provider de LLM.
- `AUDIO_PROVIDER`, `AUDIO_BASE_URL` e `AUDIO_API_KEY`: permitem separar STT/TTS do provider de chat.
- `ELEVENLABS_VOICE_ID` e `ELEVENLABS_MODEL_ID`: configuram TTS via ElevenLabs.

Configuração por tenant com fallback para `.env`:
- Você pode definir na coluna `config` da tabela `tenants`, no bloco `llm`.
- Campos suportados: `provider`, `baseUrl`, `apiKey`, `model`, `systemPrompt`, `assistantName`, `assistantIdentity`, `mcpOrchestratorEnabled`, `mcpOrchestratorMaxToolRounds`.
- Se um campo não existir no tenant, o core usa o valor global do `.env`.
- Para áudio, use `llm.audio.provider`, `llm.audio.baseUrl`, `llm.audio.apiKey`, `llm.audio.sttProvider`, `llm.audio.sttBaseUrl`, `llm.audio.sttApiKey`, `llm.audio.sttModel`, `llm.audio.ttsModel`, `llm.audio.ttsVoice`, `llm.audio.ttsInstructions`, `llm.audio.voiceId`, `llm.audio.modelId`, `llm.audio.outputFormat`, `llm.audio.stability`, `llm.audio.similarityBoost`, `llm.audio.style`, `llm.audio.speakerBoost`.

Campos de identidade do assistente por tenant:
- `assistantName`: nome oficial do assistente da empresa.
- `assistantIdentity`: texto oficial para responder quando o usuário perguntar "quem é você", "qual seu nome" ou perguntas equivalentes sobre a identidade do assistente.

### Audio no WhatsApp

Para receber audio e responder por voz:
- `WA_VOICE_REPLY_ENABLED=true|false`
- `STT_MODEL` (default `whisper-1`)
- `TTS_MODEL` (default `tts-1`)
- `TTS_VOICE` (default `alloy`)
- `AUDIO_PROVIDER` (default `openai-compatible`)
- `AUDIO_BASE_URL` e `AUDIO_API_KEY` para separar o provider de audio do provider de chat
- `AUDIO_STT_PROVIDER`, `AUDIO_STT_BASE_URL` e `AUDIO_STT_API_KEY` para STT separado do TTS
- `ELEVENLABS_VOICE_ID`, `ELEVENLABS_MODEL_ID`, `ELEVENLABS_OUTPUT_FORMAT`

Observação: a transcrição e a síntese usam `LLM_BASE_URL` e `LLM_API_KEY` em endpoints compatíveis com OpenAI (`/audio/transcriptions` e `/audio/speech`).
Se o `LLM_BASE_URL` apontar para LiteLLM, configure aliases de áudio como `harpia-stt` e `harpia-tts` no `litellm-config.yaml` e use esses nomes em `STT_MODEL` e `TTS_MODEL`.
Se `AUDIO_BASE_URL` estiver configurado, STT/TTS usam esse endpoint em vez de compartilhar o provider de chat.
Para `AUDIO_PROVIDER=elevenlabs`, apenas o TTS vai para ElevenLabs; o STT continua via provider configurado em `AUDIO_STT_PROVIDER`/`AUDIO_STT_BASE_URL`.

Exemplo de `.env` com ElevenLabs para voz e OpenAI para transcricao:
```env
AUDIO_PROVIDER=elevenlabs
AUDIO_API_KEY=elevenlabs-api-key
ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb
ELEVENLABS_MODEL_ID=eleven_multilingual_v2
ELEVENLABS_OUTPUT_FORMAT=mp3_44100_128
AUDIO_STT_PROVIDER=openai-compatible
AUDIO_STT_BASE_URL=https://api.openai.com/v1
AUDIO_STT_API_KEY=sk-...
AUDIO_STT_MODEL=whisper-1
```

### Provider Edge Agent (cloud core)

No modo atual (reverso), o core não abre conexão para o cliente. O edge-agent do cliente faz long-poll no core e recebe comandos.

Defina no core:
- `EDGE_AGENT_SHARED_TOKEN`
- `EDGE_AGENT_TIMEOUT_MS`
- `EDGE_POLL_WAIT_MS`

Recomendado por tenant no campo `config.edgeAgent` da tabela `tenants`:
- `edgeAgent.token`

### MCP no Core

O core agora expõe um endpoint MCP (JSON-RPC 2.0) para LLMs de terceiros consumirem tools operacionais.

Endpoint:
- `POST /mcp`

Autenticação opcional:
- `MCP_SERVER_TOKEN`
- Envie em `Authorization: Bearer <token>` ou `x-mcp-token`

Autenticação recomendada para cliente externo:
- As credenciais por cliente/integrador ficam na tabela `mcp_clients`.
- Cada credencial pode definir:
  - `id`
  - `name`
  - `token`
  - `allowedTenantIds`
  - `active`
- Quando `allowedTenantIds` contiver `*` ou estiver ausente/vazio, a credencial pode acessar qualquer tenant.
- Quando `allowedTenantIds` listar tenants explicitamente, `tools/call` rejeita chamadas fora dessa allowlist.
- `MCP_SERVER_TOKEN` continua válido como fallback global, sem escopo por tenant.
- Guia de entrega para cliente: `docs/mcp-client-onboarding.md`

Ativação de tools:
- As tools sao sincronizadas a partir das ToolDefinitions persistentes e do catalogo `mcp_tools` no banco.
- Defina `active: true|false` na ToolDefinition persistente ou no catalogo do banco.
- `tools/list` retorna apenas tools ativas.
- Tools inativas são rejeitadas em `tools/call`.
- `tools/call` valida os argumentos contra o `inputSchema` de cada tool e rejeita campos extras.

ToolDefinitions persistentes (`data/mcp-tools/definitions`):
- Permitem criar/alterar tools sem editar `mcpToolPolicyService.js`.
- Cada arquivo exporta uma ToolDefinition com `name`, `operation`, `active`, `description`, `inputSchema`, `labels` e `units`.
- `labels` e `units` sao opcionais e alimentam o formatter de resultado.
- Bootstrap inicial da pasta persistente: `npm run tools:bootstrap`.

Métodos suportados:
- `initialize`
- `tools/list`
- `tools/call`

Tools disponíveis:
- `get_inbound_status` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_inbound_products` (args: `tenantId`, `branchId?`, `protocolId?`, `userId?`, `channel?`, `traceId?`)
- `get_stock_summary` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_picking_status` (args: `tenantId`, `branchId?`, `userId?`, `channel?`, `traceId?`)
- `get_orders_flow_by_date` (args: `tenantId`, `branchId?`, `targetDate?`, `initialDate?`, `finalDate?`, `userId?`, `channel?`, `traceId?`)
- `get_billed_orders_count` (args: `tenantId`, `branchId?`, `targetDate?`, `initialDate?`, `finalDate?`, `userId?`, `channel?`, `traceId?`)

Formato padrao de retorno das tools:
- `schemaVersion`
- `toolName`
- `tenantId`
- `branchId`
- `metricas` (valores numericos)
- `labels` (descricao semantica de cada metrica)
- `units` (unidade de medida por metrica)

### Orquestração WhatsApp -> LLM -> MCP

Opcionalmente, no canal WhatsApp, você pode delegar a resposta para a LLM com uso de tools MCP.

Variáveis:
- `LLM_MCP_ORCHESTRATOR_ENABLED=true|false`
- `LLM_MCP_ORCHESTRATOR_MAX_TOOL_ROUNDS` (default `3`)
- `ORCHESTRATION_MODE_DEFAULT=rule-engine|llm-basic|llm-mcp`

Configuração por tenant em `data/tenants.json`:
- `orchestration.mode=rule-engine|llm-basic|llm-mcp`

Precedência de modo:
1. `tenant.orchestration.mode`
2. `ORCHESTRATION_MODE_DEFAULT`
3. fallback compatível: se `LLM_MCP_ORCHESTRATOR_ENABLED=true` no WhatsApp, usa `llm-mcp`
4. padrão final: `rule-engine`

Quando habilitado:
1. mensagem entra no WhatsApp
2. LLM decide e chama apenas as tools ativas (`get_inbound_status`, `get_inbound_products`, `get_stock_summary`, `get_picking_status`, `get_orders_flow_by_date`, `get_billed_orders_count`)
3. core executa tool por tenant e devolve para a LLM
4. resposta final padronizada volta ao WhatsApp

### Provider PostgreSQL

Defina `POSTGRES_URL` ou os campos `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`.

As consultas padrão assumem tabelas:
- `inbound_tasks`
- `stock_daily_summary`
- `picking_orders`

Você pode sobrescrever qualquer consulta com:
- `POSTGRES_QUERY_INBOUND`
- `POSTGRES_QUERY_INBOUND_PRODUCTS`
- `POSTGRES_QUERY_STOCK`
- `POSTGRES_QUERY_PICKING`
- `POSTGRES_QUERY_ORDERS_FLOW_BY_DATE`
- `POSTGRES_QUERY_BILLED_ORDERS_COUNT`

### Provider Oracle

Para Oracle, instale a dependencia:

```bash
npm install oracledb
```

Defina `ORACLE_CONNECT_STRING` (ou `ORACLE_HOST`, `ORACLE_PORT`, `ORACLE_SERVICE_NAME`) e credenciais `ORACLE_USER` e `ORACLE_PASSWORD`.

Você pode sobrescrever qualquer consulta com:
- `ORACLE_QUERY_INBOUND`
- `ORACLE_QUERY_INBOUND_PRODUCTS`
- `ORACLE_QUERY_STOCK`
- `ORACLE_QUERY_PICKING`
- `ORACLE_QUERY_ORDERS_FLOW_BY_DATE`
- `ORACLE_QUERY_BILLED_ORDERS_COUNT`

As consultas padrao assumem tabelas:
- `inbound_tasks`
- `stock_daily_summary`
- `picking_orders`
- `orders`

Voce pode sobrescrever qualquer consulta com:
- `ORACLE_QUERY_INBOUND`
- `ORACLE_QUERY_STOCK`
- `ORACLE_QUERY_PICKING`
- `ORACLE_QUERY_ORDERS_FLOW_BY_DATE`
- `ORACLE_QUERY_BILLED_ORDERS_COUNT`

## Entrada por CLI

```bash
npm run chat
```

Parâmetros opcionais:

```bash
npm run chat -- --tenant cliente-demo --user operador-01 --name "Carlos"
```

## Edge Agent local (cliente)

Suba um processo separado no ambiente do cliente:

```bash
npm run start:edge-agent
```

Modo recomendado (`EDGE_AGENT_MODE=reverse-client`):
- o cliente conecta no core em `EDGE_CORE_URL`
- faz poll em `POST /edge/poll`
- responde em `POST /edge/result`
- executa queries no backend local (`EDGE_DATA_BACKEND=postgres|oracle|mock`)
- resolve operacoes dinamicamente por convencao (`snake_case` -> `getPascalCase`) para reduzir manutencao no server do agente

Variáveis do edge-agent local:
- `EDGE_AGENT_MODE=reverse-client`
- `EDGE_CORE_URL=http://host-do-core:3000`
- `EDGE_AGENT_TENANT_ID=cliente-demo`
- `EDGE_AGENT_TOKEN` (ou `EDGE_AGENT_SHARED_TOKEN`)
- `EDGE_AGENT_HTTP_TIMEOUT_MS`
- `EDGE_AGENT_RECONNECT_DELAY_MS`
- `EDGE_AGENT_CONCURRENCY` (quantidade de comandos processados em paralelo)
- `EDGE_AGENT_MAX_QUEUE_SIZE` (limite da fila local antes de rejeitar novos comandos)

Modo legado opcional (`EDGE_AGENT_MODE=legacy-server`):
- mantém rotas `GET /health` e `POST /agent/query` para core conectar direto no cliente.

## Comandos já suportados

- `recebimento`
- `estoque`
- `separacao`
- `minhas filiais`
- `trocar filial`
- `ajuda`

## Próximo passo recomendado

Trocar o adapter `mockOperationsDataProvider.js` por adapters reais de Oracle, MCP, PostgreSQL ou outro backend, sem alterar o caso de uso central.