@lionchat/mcp-server 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/instructions.md +57 -7
- package/dist/docs/prompts/customer_health.json +1 -1
- package/dist/docs/prompts/inactive_contacts.json +1 -1
- package/dist/docs/prompts/productivity_report.json +1 -1
- package/dist/docs/prompts/team_load_balance.json +1 -1
- package/dist/docs/resources/api-conventions.md +11 -3
- package/dist/docs/resources/best-practices.md +40 -6
- package/dist/docs/resources/conversation-flows.md +96 -0
- package/dist/docs/resources/data-model.md +18 -4
- package/dist/docs/resources/flowbuilder-design-guide.md +134 -16
- package/dist/docs/resources/flowbuilder-patterns.md +19 -18
- package/dist/docs/resources/glossary.md +29 -2
- package/dist/docs/resources/reports-guide.md +164 -85
- package/endpoints.json +711 -676
- package/package.json +1 -1
|
@@ -26,6 +26,8 @@ Como criar fluxos do LionChat (FlowBuilder) que abrem certinhos no canvas, sem n
|
|
|
26
26
|
| Distribuir aleatório entre branches | `randomizer` | - |
|
|
27
27
|
| Atualizar info de grupo WhatsApp (WAHA) | `update_group` | - |
|
|
28
28
|
| Iniciar outro fluxo | `action` com `key: 'start_flow'` OU node `activate_flow` | - |
|
|
29
|
+
| Encerrar ramo / definir retorno de ai_tool | `end` | - |
|
|
30
|
+
| Anotação visual no canvas (não executa) | `note` | - |
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -52,13 +54,19 @@ Todo node tem essa estrutura base:
|
|
|
52
54
|
"data": {
|
|
53
55
|
"label": "Início",
|
|
54
56
|
"triggers": [
|
|
55
|
-
{ "type": "message_received", "keywords": ["oi", "ola"], "
|
|
57
|
+
{ "type": "message_received", "keywords": ["oi", "ola"], "match_type": "contains" }
|
|
56
58
|
]
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
**Triggers válidos:** `message_received`, `conversation_created`, `conversation_resolved`, `label_added`, `label_removed`, `cron`, `webhook`.
|
|
63
|
+
**Triggers válidos:** `message_received`, `conversation_created`, `conversation_resolved`, `conversation_reopened`, `label_added`, `label_removed`, `card_created`, `card_moved`, `cron`, `webhook`.
|
|
64
|
+
|
|
65
|
+
**Campos de filtro por trigger (IMPORTANTE — nomes exatos):**
|
|
66
|
+
- `message_received`: `keywords` (array) + `match_type` (`'exact'` ou `'contains'`, default `contains`). NÃO use `match_mode` aqui.
|
|
67
|
+
- `conversation_created` / `conversation_reopened`: filtro opcional de keywords via `match_mode` (`'any'`, `'contains'`, `'exact'`, `'customer_initiated'`, `'agent_initiated'`) + `keywords`. Só ESTES dois triggers usam `match_mode`.
|
|
68
|
+
- `label_added` / `label_removed`: `label_names` (array de slugs). NÃO use `label` (singular) — é ignorado.
|
|
69
|
+
- `card_created` / `card_moved`: `funnel_ids` (array) + `funnel_stages` (array de `"funnel_id:stage"`).
|
|
62
70
|
|
|
63
71
|
**Handles que SAEM:** `success`.
|
|
64
72
|
|
|
@@ -80,11 +88,20 @@ Todo node tem essa estrutura base:
|
|
|
80
88
|
}
|
|
81
89
|
```
|
|
82
90
|
|
|
83
|
-
**Tipos de item válidos:** `text`, `whatsapp_template` (ou `template`), `canned_response`, `user_input` (
|
|
91
|
+
**Tipos de item válidos:** `text`, `whatsapp_template` (ou `template`), `canned_response`, `user_input` (pausa esperando resposta livre), `delay`, `attachment`, `audio`.
|
|
84
92
|
|
|
85
93
|
**ATENÇÃO:** usa `messageItems` (NÃO `items`).
|
|
86
94
|
|
|
87
|
-
**
|
|
95
|
+
**Botões interativos:** um item `text` com `buttons_enabled: true` e `buttons: [{ title, value }, ...]` vira mensagem com botões. Ao clicar, o flow roteia pelo handle **`button_<value>`** (ex: botão com `value: "sim"` → handle `button_sim`). Se o cliente digitar texto livre em vez de clicar, cai no handle **`no_response`**. Se houver timeout configurado, cai em **`no_reply_timeout`**.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "id": "m1", "type": "text", "content": "Confirma o agendamento?",
|
|
99
|
+
"buttons_enabled": true,
|
|
100
|
+
"buttons": [ { "title": "Sim", "value": "sim" }, { "title": "Não", "value": "nao" } ] }
|
|
101
|
+
```
|
|
102
|
+
→ edges: `sourceHandle: "button_sim"`, `sourceHandle: "button_nao"`, e opcionalmente `"no_response"`.
|
|
103
|
+
|
|
104
|
+
**Handles que SAEM:** `success` (sem botões); com botões → `button_<value>` (um por botão) + `no_response` (+ `no_reply_timeout` se tiver timeout). Também `error`.
|
|
88
105
|
|
|
89
106
|
### 2.3 `wait_response`
|
|
90
107
|
|
|
@@ -101,7 +118,7 @@ Todo node tem essa estrutura base:
|
|
|
101
118
|
"acceptedOptions": ["1", "2", "3"],
|
|
102
119
|
"invalidMessage": "Por favor responda com 1, 2 ou 3",
|
|
103
120
|
"maxRetries": 3,
|
|
104
|
-
"saveTo": "
|
|
121
|
+
"saveTo": "conversation_attr",
|
|
105
122
|
"saveAttrKey": "escolha_menu"
|
|
106
123
|
}
|
|
107
124
|
}
|
|
@@ -109,7 +126,16 @@ Todo node tem essa estrutura base:
|
|
|
109
126
|
|
|
110
127
|
**`validation` válidos:** `any`, `options`, `regex`.
|
|
111
128
|
|
|
112
|
-
**`saveTo` válidos
|
|
129
|
+
**`saveTo` válidos (nomes EXATOS — qualquer outro valor NÃO salva nada):**
|
|
130
|
+
- `variable` — variável temporária do flow (use `saveVariable` pra nomear; senão usa o próprio `saveTo`)
|
|
131
|
+
- `contact_name` — sobrescreve o nome do contato
|
|
132
|
+
- `contact_email` — sobrescreve o email do contato
|
|
133
|
+
- `contact_phone` — sobrescreve o telefone do contato
|
|
134
|
+
- `contact_attr` — custom attribute do CONTATO (precisa de `saveAttrKey`)
|
|
135
|
+
- `conversation_attr` — custom attribute da CONVERSA (precisa de `saveAttrKey`)
|
|
136
|
+
- `""` — não salva
|
|
137
|
+
|
|
138
|
+
**NÃO existem** `attribute` nem `contact_attribute` — use `conversation_attr` / `contact_attr`.
|
|
113
139
|
|
|
114
140
|
**Handles que SAEM dependem da validation:**
|
|
115
141
|
- `validation: 'any'` → `success`, `timeout`
|
|
@@ -128,18 +154,47 @@ Todo node tem essa estrutura base:
|
|
|
128
154
|
"data": {
|
|
129
155
|
"label": "Tipo de cliente",
|
|
130
156
|
"conditions": [
|
|
131
|
-
{ "id": "c1", "label": "VIP", "field": "contact.
|
|
132
|
-
{ "id": "c2", "label": "Padrão", "field": "contact.
|
|
157
|
+
{ "id": "c1", "label": "VIP", "field": "contact.custom_attribute.plano", "operator": "equal", "value": "premium" },
|
|
158
|
+
{ "id": "c2", "label": "Padrão", "field": "contact.custom_attribute.plano", "operator": "equal", "value": "standard" }
|
|
133
159
|
]
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
```
|
|
137
163
|
|
|
138
|
-
**
|
|
164
|
+
**ATENÇÃO — atributo customizado é SINGULAR:** `contact.custom_attribute.X` e `conversation.custom_attribute.X` (também `account.custom_attribute.X`). Usar plural `custom_attributes` resolve VAZIO — vale tanto no `field` da condição quanto em mensagens/variáveis `{{...}}`.
|
|
165
|
+
|
|
166
|
+
**Operadores válidos (lista real do runtime):**
|
|
167
|
+
|
|
168
|
+
| Operador | Uso |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `equal` (ou `field_equals`) / `not_equal` | igualdade |
|
|
171
|
+
| `contains` / `not_contains` | substring |
|
|
172
|
+
| `starts_with` / `ends_with` | prefixo/sufixo |
|
|
173
|
+
| `is_empty` / `is_not_empty` | vazio/preenchido (NÃO existe `is_present`/`is_blank`) |
|
|
174
|
+
| `greater_than` / `less_than` | comparação numérica |
|
|
175
|
+
| `number_range` | faixa; `value` no formato `"min-max"` (ex `"10-50"`) |
|
|
176
|
+
| `has_length` | comprimento exato (`value` = número) |
|
|
177
|
+
| `is_number` / `is_letter` / `is_email` / `is_phone` | validação de formato |
|
|
178
|
+
| `regex` | padrão regex em `value` |
|
|
179
|
+
| `equal_any` / `not_equal_any` / `contains_any` | multi-valor (usa `values` array) |
|
|
180
|
+
| `business_hours` / `outside_business_hours` | horário comercial |
|
|
181
|
+
| `can_reply` / `can_reply_closed` | janela 24h aberta/fechada |
|
|
182
|
+
| `conversation_has_agent` / `conversation_no_agent` / `conversation_not_agent` | agente atribuído |
|
|
183
|
+
| `contact_has_label` / `contact_no_label` / `conversation_has_label` / `conversation_no_label` | labels |
|
|
184
|
+
| `kanban_exists` / `kanban_in_stage` / `kanban_won` / `kanban_lost` | card no funil (usa `funnel_id` + `stage`) |
|
|
185
|
+
| `card_attr_equals` / `card_attr_contains` | atributo do card (`attrSource: 'card'` + `attr_key`) |
|
|
186
|
+
| `pagetrack_visited` / `pagetrack_event` | LionTrack |
|
|
187
|
+
|
|
188
|
+
**Restrição por TIPO de atributo (a UI só oferece um subconjunto, e é o que faz sentido):**
|
|
189
|
+
- **Texto/string:** `equal`, `not_equal`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty`
|
|
190
|
+
- **Número:** `equal`, `not_equal`, `greater_than`, `less_than`, `number_range`, `is_empty`, `is_not_empty`
|
|
191
|
+
- **Lista/Data:** `equal`, `not_equal`, `contains`, `not_contains`
|
|
192
|
+
|
|
193
|
+
Use operador numérico (`greater_than`, `less_than`, `number_range`) SÓ em atributo de tipo número.
|
|
139
194
|
|
|
140
195
|
**Handles que SAEM:** `cond_0`, `cond_1`, `cond_2`, ... (UM POR CONDIÇÃO, pela ordem do array — NÃO use o `id` da condição) + `default` (quando nenhuma bate).
|
|
141
196
|
|
|
142
|
-
**ATENÇÃO:**
|
|
197
|
+
**ATENÇÃO — lógica first-match-wins (if / elsif):** as condições são avaliadas em ordem e PARA na primeira que bate. A ordem do array importa. O handle é `cond_INDEX` baseado na posição, NÃO o `id` da condição. Se `conditions[0].id = "vip"`, o handle ainda é `cond_0`.
|
|
143
198
|
|
|
144
199
|
### 2.5 `action`
|
|
145
200
|
|
|
@@ -184,12 +239,28 @@ Todo node tem essa estrutura base:
|
|
|
184
239
|
| `send_webhook` | `{ url, headers?, body? }` | Dispara webhook externo |
|
|
185
240
|
| `start_flow` | `{ flow_id }` | Inicia outro fluxo |
|
|
186
241
|
| `deactivate_flow` ou `disable_flow` | `{}` | Encerra fluxo atual |
|
|
187
|
-
| `update_attribute` | `{
|
|
242
|
+
| `update_attribute` | `{ attr_source: 'contact'\|'conversation'\|'card', attr_key, attr_value }` | Seta custom_attribute (ver abaixo) |
|
|
188
243
|
| `assign_captain` (ou `assign_captain_assistant`) | `{ assistant_id }` | Atribui IA Captain |
|
|
189
244
|
| `deactivate_captain` | `{}` | Tira a IA da conversa |
|
|
190
245
|
|
|
191
246
|
**Handles que SAEM:** `success`. Não tem handle `error` — falhas viram warning silencioso e o flow continua.
|
|
192
247
|
|
|
248
|
+
**`update_attribute` — campos EXATOS:** `attr_source` (`'contact'`, `'conversation'` ou `'card'`), `attr_key` (nome do atributo), `attr_value` (valor). NÃO existem `entity`/`key`/`value` — esses são ignorados e não salvam nada.
|
|
249
|
+
|
|
250
|
+
**Somar/subtrair NÃO é operação dedicada** — é filtro Liquid no próprio `attr_value`. O valor é resolvido como template antes de salvar. Exemplos:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{ "key": "update_attribute", "config": {
|
|
254
|
+
"attr_source": "contact", "attr_key": "numero_tokens",
|
|
255
|
+
"attr_value": "{{ contact.custom_attribute.numero_tokens | minus: 1 }}"
|
|
256
|
+
} }
|
|
257
|
+
```
|
|
258
|
+
- Subtrair 1 token: `{{ contact.custom_attribute.numero_tokens | minus: 1 }}`
|
|
259
|
+
- Somar 5: `{{ conversation.custom_attribute.pontos | plus: 5 }}`
|
|
260
|
+
- Multiplicar: `| times: 2` · Dividir: `| divided_by: 2`
|
|
261
|
+
|
|
262
|
+
Lembre: o atributo lido é SINGULAR (`custom_attribute`).
|
|
263
|
+
|
|
193
264
|
### 2.6 `api`
|
|
194
265
|
|
|
195
266
|
```json
|
|
@@ -223,18 +294,24 @@ Todo node tem essa estrutura base:
|
|
|
223
294
|
"data": {
|
|
224
295
|
"label": "Classifica intenção",
|
|
225
296
|
"aiMode": "intent",
|
|
226
|
-
"aiPrompt": "Classifique a intenção da última mensagem
|
|
227
|
-
"
|
|
228
|
-
|
|
297
|
+
"aiPrompt": "Classifique a intenção da última mensagem",
|
|
298
|
+
"aiIntents": [
|
|
299
|
+
{ "name": "compra" },
|
|
300
|
+
{ "name": "suporte" },
|
|
301
|
+
{ "name": "reclamacao" },
|
|
302
|
+
{ "name": "outro" }
|
|
303
|
+
]
|
|
229
304
|
}
|
|
230
305
|
}
|
|
231
306
|
```
|
|
232
307
|
|
|
233
308
|
**`aiMode` válidos:** `generate`, `intent`, `sentiment`, `extract`.
|
|
234
309
|
|
|
310
|
+
**Intent — campo EXATO:** `aiIntents` é um ARRAY DE OBJETOS `{ "name": "..." }`. NÃO use `aiIntentOptions` (array de strings) — é ignorado. A intenção classificada também fica disponível na variável de sessão **`ai_intent`** (use como `{{ai_intent}}` adiante).
|
|
311
|
+
|
|
235
312
|
**Handles que SAEM dependem do mode:**
|
|
236
313
|
- `generate` → `success`, `error`
|
|
237
|
-
- `intent` → uma saída por
|
|
314
|
+
- `intent` → uma saída por intenção (`intent_<name>`, ex: `intent_compra`, `intent_suporte`) **+ `no_intent`** (quando nenhuma bate) + `error`
|
|
238
315
|
- `sentiment` → `positive`, `negative`, `neutral` + `error`
|
|
239
316
|
- `extract` → `success`, `error` (resultado salvo em `aiResponseVar`)
|
|
240
317
|
|
|
@@ -270,7 +347,7 @@ Pra `weekday`, use também `targetWeekday` (0=domingo, 1=segunda... 6=sábado) e
|
|
|
270
347
|
"label": "Define contexto",
|
|
271
348
|
"variables": [
|
|
272
349
|
{ "name": "origem_lead", "value": "Facebook Ads" },
|
|
273
|
-
{ "name": "score", "value": "{{contact.
|
|
350
|
+
{ "name": "score", "value": "{{contact.custom_attribute.lead_score}}" }
|
|
274
351
|
]
|
|
275
352
|
}
|
|
276
353
|
}
|
|
@@ -317,6 +394,38 @@ Atualiza nome/descrição/foto de grupo WhatsApp. Use só quando flow roda em in
|
|
|
317
394
|
|
|
318
395
|
**Handles que SAEM:** `success`, `error`.
|
|
319
396
|
|
|
397
|
+
### 2.13 `end` (encerra ramo / define retorno do ai_tool)
|
|
398
|
+
|
|
399
|
+
Node terminal, sem handles de saída. Em flow `conversation` apenas encerra aquele ramo. Em flow `ai_tool` é OBRIGATÓRIO: o `data` do `end` define o que volta pro LLM (modo de saída + template do resultado).
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{ "id": "node-end", "type": "end", "position": { "x": 1330, "y": 300 }, "data": { "label": "Retorno", "mode": "structured" } }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### 2.14 `note` (anotação visual)
|
|
406
|
+
|
|
407
|
+
Sticky note no canvas, puramente visual. Sem handles, nunca executado (não ligue edges nele). Serve só pra documentar o flow.
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{ "id": "note-1", "type": "note", "position": { "x": 50, "y": 50 }, "data": { "content": "Fluxo de qualificação — revisar mensagens" } }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 2-B. Dois tipos de flow: `conversation` vs `ai_tool`
|
|
416
|
+
|
|
417
|
+
O campo `flow_type` (definido na criação, IMUTÁVEL depois) decide a natureza do flow:
|
|
418
|
+
|
|
419
|
+
| | `conversation` (default) | `ai_tool` |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| Como dispara | Por evento de inbox (trigger no node `start`) | Invocado pelo AI Agente (Captain) como ferramenta |
|
|
422
|
+
| Inboxes | usa `inbox_ids` | **PROIBIDO** ter inboxes (validação barra) |
|
|
423
|
+
| Campos extra | — | `tool_name` (snake_case, `[a-z][a-z0-9_]`, max 50) + `tool_description` (max 500) OBRIGATÓRIOS |
|
|
424
|
+
| Retorno | manda mensagens | retorna dado estruturado ao LLM via node `end` |
|
|
425
|
+
| Nodes permitidos | todos | SÓ: `start`, `end`, `api`, `condition`, `set_variable`, `ai`, `note` |
|
|
426
|
+
|
|
427
|
+
Se o cliente pediu "uma ferramenta que a IA usa pra consultar X / calcular Y", é `ai_tool`. Se pediu "quando chega mensagem, faça Z", é `conversation`. Na dúvida, `conversation`.
|
|
428
|
+
|
|
320
429
|
---
|
|
321
430
|
|
|
322
431
|
## 3. Edges
|
|
@@ -400,6 +509,15 @@ Nodes nunca devem ficar com a mesma coordenada `(x, y)`. Se dois nodes têm posi
|
|
|
400
509
|
| Edge com `target` apontando pra ID que não existe | Quebra o grafo | Confira que `target` está em `nodes[]` |
|
|
401
510
|
| `channel_type: "WhatsApp"` | Precisa do nome de classe Rails | `"Channel::Whatsapp"`, `"Channel::Waha"`, `"Channel::WebWidget"` |
|
|
402
511
|
| `validation: "option"` (singular) | Não existe | `"options"` (plural) |
|
|
512
|
+
| `custom_attributes` (plural) em `{{...}}` ou `field` | Resolve vazio | `custom_attribute` (singular) |
|
|
513
|
+
| `update_attribute` com `{entity, key, value}` | Campos errados, não salva | `{attr_source, attr_key, attr_value}` |
|
|
514
|
+
| `saveTo: "attribute"` ou `"contact_attribute"` | Não existem, não salva | `"conversation_attr"` / `"contact_attr"` |
|
|
515
|
+
| `is_present` / `is_blank` na condition | Não existem | `is_not_empty` / `is_empty` |
|
|
516
|
+
| `aiIntentOptions` (array de strings) no node ai | Ignorado | `aiIntents: [{name}]` |
|
|
517
|
+
| `match_mode` no `message_received` | Ignorado | `match_type` (`exact`/`contains`) |
|
|
518
|
+
| `label` (singular) em `label_added`/`label_removed` | Ignorado | `label_names: [...]` |
|
|
519
|
+
| operador numérico (`greater_than` etc) em atributo texto | UI não oferece; semântica errada | usar só em atributo número |
|
|
520
|
+
| inboxes em flow `ai_tool` | Validação rejeita | ai_tool não tem inboxes |
|
|
403
521
|
| `waitTime: "60"` (string) | Espera Integer | `waitTime: 60` |
|
|
404
522
|
| `inbox_ids` aninhado em `{flow:{...}}` | No MCP, achata-se sozinho | Passa `inbox_ids: [1, 2]` no nível raiz |
|
|
405
523
|
| Mais de 1 node `start` | Flow precisa ter exatamente 1 ponto de entrada | Só 1 |
|
|
@@ -61,15 +61,15 @@
|
|
|
61
61
|
] } },
|
|
62
62
|
{ "id": "n3", "type": "wait_response", "position": { "x": 690, "y": 300 }, "data": {
|
|
63
63
|
"label": "Aguarda nome", "waitTime": 15, "waitUnit": "minutes", "validation": "any",
|
|
64
|
-
"saveTo": "
|
|
64
|
+
"saveTo": "contact_attr", "saveAttrKey": "nome_completo"
|
|
65
65
|
} },
|
|
66
66
|
{ "id": "n4", "type": "send_message", "position": { "x": 1010, "y": 300 }, "data": { "label": "Pede email", "messageItems": [
|
|
67
|
-
{ "id": "m2", "type": "text", "content": "Perfeito, {{contact.
|
|
67
|
+
{ "id": "m2", "type": "text", "content": "Perfeito, {{contact.custom_attribute.nome_completo}}. Qual seu melhor email?" }
|
|
68
68
|
] } },
|
|
69
69
|
{ "id": "n5", "type": "wait_response", "position": { "x": 1330, "y": 300 }, "data": {
|
|
70
70
|
"label": "Aguarda email", "waitTime": 15, "waitUnit": "minutes", "validation": "regex",
|
|
71
71
|
"regexPattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "invalidMessage": "Esse não parece um email válido. Tenta de novo?", "maxRetries": 2,
|
|
72
|
-
"saveTo": "
|
|
72
|
+
"saveTo": "contact_attr", "saveAttrKey": "email"
|
|
73
73
|
} },
|
|
74
74
|
{ "id": "n6", "type": "action", "position": { "x": 1650, "y": 300 }, "data": { "label": "Cria card", "items": [
|
|
75
75
|
{ "key": "create_kanban_item", "config": { "funnel_id": 1, "funnel_stage": "novo_lead" } },
|
|
@@ -99,17 +99,17 @@
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
101
|
"nodes": [
|
|
102
|
-
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "message_received", "keywords": ["proposta","orçamento"], "
|
|
102
|
+
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "message_received", "keywords": ["proposta","orçamento"], "match_type": "contains" }] } },
|
|
103
103
|
{ "id": "n2", "type": "send_message", "position": { "x": 370, "y": 300 }, "data": { "label": "B - Budget", "messageItems": [{ "id": "m1", "type": "text", "content": "Pra te ajudar melhor, qual a faixa de investimento que vocês têm em mente?\n\n1️⃣ Até R$ 5k\n2️⃣ R$ 5k a R$ 20k\n3️⃣ Acima de R$ 20k" }] } },
|
|
104
|
-
{ "id": "n3", "type": "wait_response", "position": { "x": 690, "y": 300 }, "data": { "label": "Budget", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3"], "saveTo": "
|
|
104
|
+
{ "id": "n3", "type": "wait_response", "position": { "x": 690, "y": 300 }, "data": { "label": "Budget", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3"], "saveTo": "contact_attr", "saveAttrKey": "budget_tier" } },
|
|
105
105
|
{ "id": "n4", "type": "send_message", "position": { "x": 1010, "y": 300 }, "data": { "label": "A - Authority", "messageItems": [{ "id": "m2", "type": "text", "content": "Você é o decisor ou tem outras pessoas envolvidas?" }] } },
|
|
106
|
-
{ "id": "n5", "type": "wait_response", "position": { "x": 1330, "y": 300 }, "data": { "label": "Autoridade", "waitTime": 60, "waitUnit": "minutes", "validation": "any", "saveTo": "
|
|
106
|
+
{ "id": "n5", "type": "wait_response", "position": { "x": 1330, "y": 300 }, "data": { "label": "Autoridade", "waitTime": 60, "waitUnit": "minutes", "validation": "any", "saveTo": "contact_attr", "saveAttrKey": "authority" } },
|
|
107
107
|
{ "id": "n6", "type": "send_message", "position": { "x": 1650, "y": 300 }, "data": { "label": "N - Need", "messageItems": [{ "id": "m3", "type": "text", "content": "Conta um pouco: o que vocês precisam resolver hoje?" }] } },
|
|
108
|
-
{ "id": "n7", "type": "wait_response", "position": { "x": 1970, "y": 300 }, "data": { "label": "Necessidade", "waitTime": 60, "waitUnit": "minutes", "validation": "any", "saveTo": "
|
|
108
|
+
{ "id": "n7", "type": "wait_response", "position": { "x": 1970, "y": 300 }, "data": { "label": "Necessidade", "waitTime": 60, "waitUnit": "minutes", "validation": "any", "saveTo": "contact_attr", "saveAttrKey": "need" } },
|
|
109
109
|
{ "id": "n8", "type": "send_message", "position": { "x": 2290, "y": 300 }, "data": { "label": "T - Timing", "messageItems": [{ "id": "m4", "type": "text", "content": "Em quanto tempo precisam ter a solução rodando?\n\n1️⃣ Esse mês\n2️⃣ Próximos 3 meses\n3️⃣ Sem pressa" }] } },
|
|
110
|
-
{ "id": "n9", "type": "wait_response", "position": { "x": 2610, "y": 300 }, "data": { "label": "Prazo", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3"], "saveTo": "
|
|
110
|
+
{ "id": "n9", "type": "wait_response", "position": { "x": 2610, "y": 300 }, "data": { "label": "Prazo", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3"], "saveTo": "contact_attr", "saveAttrKey": "timing_tier" } },
|
|
111
111
|
{ "id": "n10", "type": "condition", "position": { "x": 2930, "y": 300 }, "data": { "label": "Quente?", "conditions": [
|
|
112
|
-
{ "id": "c1", "label": "Lead quente", "field": "contact.
|
|
112
|
+
{ "id": "c1", "label": "Lead quente", "field": "contact.custom_attribute.budget_tier", "operator": "not_equal", "value": "1" }
|
|
113
113
|
] } },
|
|
114
114
|
{ "id": "n11", "type": "action", "position": { "x": 3250, "y": 180 }, "data": { "label": "Quente", "items": [
|
|
115
115
|
{ "key": "assign_team", "config": { "team_id": 1 } },
|
|
@@ -153,9 +153,9 @@
|
|
|
153
153
|
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "conversation_resolved" }] } },
|
|
154
154
|
{ "id": "n2", "type": "wait", "position": { "x": 370, "y": 300 }, "data": { "label": "Aguarda 5 min", "waitTime": 5, "waitUnit": "minutes" } },
|
|
155
155
|
{ "id": "n3", "type": "send_message", "position": { "x": 690, "y": 300 }, "data": { "label": "Pede nota", "messageItems": [{ "id": "m1", "type": "text", "content": "Antes de fechar: de 1 a 5, como foi seu atendimento?\n\n5 = excelente\n1 = ruim" }] } },
|
|
156
|
-
{ "id": "n4", "type": "wait_response", "position": { "x": 1010, "y": 300 }, "data": { "label": "Nota", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3","4","5"], "saveTo": "
|
|
156
|
+
{ "id": "n4", "type": "wait_response", "position": { "x": 1010, "y": 300 }, "data": { "label": "Nota", "waitTime": 60, "waitUnit": "minutes", "validation": "options", "acceptedOptions": ["1","2","3","4","5"], "saveTo": "conversation_attr", "saveAttrKey": "csat_score" } },
|
|
157
157
|
{ "id": "n5", "type": "send_message", "position": { "x": 1330, "y": 300 }, "data": { "label": "Pede comentário", "messageItems": [{ "id": "m2", "type": "text", "content": "Obrigado pela nota! Quer deixar algum comentário?" }] } },
|
|
158
|
-
{ "id": "n6", "type": "wait_response", "position": { "x": 1650, "y": 300 }, "data": { "label": "Comentário", "waitTime": 30, "waitUnit": "minutes", "validation": "any", "saveTo": "
|
|
158
|
+
{ "id": "n6", "type": "wait_response", "position": { "x": 1650, "y": 300 }, "data": { "label": "Comentário", "waitTime": 30, "waitUnit": "minutes", "validation": "any", "saveTo": "conversation_attr", "saveAttrKey": "csat_comment" } },
|
|
159
159
|
{ "id": "n7", "type": "send_message", "position": { "x": 1970, "y": 300 }, "data": { "label": "Encerra", "messageItems": [{ "id": "m3", "type": "text", "content": "Valeu pelo feedback! 🙏" }] } }
|
|
160
160
|
],
|
|
161
161
|
"edges": [
|
|
@@ -214,20 +214,21 @@
|
|
|
214
214
|
{ "id": "n2", "type": "ai", "position": { "x": 370, "y": 300 }, "data": {
|
|
215
215
|
"label": "Classifica", "aiMode": "intent",
|
|
216
216
|
"aiPrompt": "Classifique a intenção da última mensagem do cliente",
|
|
217
|
-
"
|
|
218
|
-
"aiResponseVar": "intent"
|
|
217
|
+
"aiIntents": [{ "name": "compra" }, { "name": "suporte" }, { "name": "reclamacao" }, { "name": "duvida" }]
|
|
219
218
|
} },
|
|
220
219
|
{ "id": "n3", "type": "action", "position": { "x": 690, "y": 60 }, "data": { "label": "→ Compra", "items": [{ "key": "assign_team", "config": { "team_id": 1 } }, { "key": "add_label", "config": { "labels": ["intent-compra"] } }] } },
|
|
221
220
|
{ "id": "n4", "type": "action", "position": { "x": 690, "y": 240 }, "data": { "label": "→ Suporte", "items": [{ "key": "assign_team", "config": { "team_id": 2 } }, { "key": "add_label", "config": { "labels": ["intent-suporte"] } }] } },
|
|
222
221
|
{ "id": "n5", "type": "action", "position": { "x": 690, "y": 420 }, "data": { "label": "→ Reclamação", "items": [{ "key": "assign_team", "config": { "team_id": 3 } }, { "key": "change_priority", "config": { "priority": "high" } }, { "key": "add_label", "config": { "labels": ["intent-reclamacao"] } }] } },
|
|
223
|
-
{ "id": "n6", "type": "action", "position": { "x": 690, "y": 600 }, "data": { "label": "→ Dúvida", "items": [{ "key": "assign_captain", "config": { "assistant_id": 1 } }] } }
|
|
222
|
+
{ "id": "n6", "type": "action", "position": { "x": 690, "y": 600 }, "data": { "label": "→ Dúvida", "items": [{ "key": "assign_captain", "config": { "assistant_id": 1 } }] } },
|
|
223
|
+
{ "id": "n7", "type": "action", "position": { "x": 690, "y": 780 }, "data": { "label": "→ Não classificado", "items": [{ "key": "assign_team", "config": { "team_id": 2 } }] } }
|
|
224
224
|
],
|
|
225
225
|
"edges": [
|
|
226
226
|
{ "id": "e1", "source": "n1", "target": "n2", "sourceHandle": "success", "type": "deletable", "animated": true },
|
|
227
227
|
{ "id": "e2", "source": "n2", "target": "n3", "sourceHandle": "intent_compra", "type": "deletable", "animated": true },
|
|
228
228
|
{ "id": "e3", "source": "n2", "target": "n4", "sourceHandle": "intent_suporte", "type": "deletable", "animated": true },
|
|
229
229
|
{ "id": "e4", "source": "n2", "target": "n5", "sourceHandle": "intent_reclamacao", "type": "deletable", "animated": true },
|
|
230
|
-
{ "id": "e5", "source": "n2", "target": "n6", "sourceHandle": "intent_duvida", "type": "deletable", "animated": true }
|
|
230
|
+
{ "id": "e5", "source": "n2", "target": "n6", "sourceHandle": "intent_duvida", "type": "deletable", "animated": true },
|
|
231
|
+
{ "id": "e6", "source": "n2", "target": "n7", "sourceHandle": "no_intent", "type": "deletable", "animated": true }
|
|
231
232
|
]
|
|
232
233
|
}
|
|
233
234
|
```
|
|
@@ -241,7 +242,7 @@
|
|
|
241
242
|
```json
|
|
242
243
|
{
|
|
243
244
|
"nodes": [
|
|
244
|
-
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "label_added", "
|
|
245
|
+
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "label_added", "label_names": ["follow-up"] }] } },
|
|
245
246
|
{ "id": "n2", "type": "wait", "position": { "x": 370, "y": 300 }, "data": { "label": "Espera 7 dias", "waitTime": 7, "waitUnit": "days" } },
|
|
246
247
|
{ "id": "n3", "type": "send_message", "position": { "x": 690, "y": 300 }, "data": { "label": "Template re-engajamento", "messageItems": [
|
|
247
248
|
{ "id": "m1", "type": "whatsapp_template", "templateId": 42, "params": ["{{contact.name}}"] }
|
|
@@ -298,7 +299,7 @@
|
|
|
298
299
|
```json
|
|
299
300
|
{
|
|
300
301
|
"nodes": [
|
|
301
|
-
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "message_received", "keywords": ["agendar","reunião"], "
|
|
302
|
+
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "message_received", "keywords": ["agendar","reunião"], "match_type": "contains" }] } },
|
|
302
303
|
{ "id": "n2", "type": "send_message", "position": { "x": 370, "y": 300 }, "data": { "label": "Pede data", "messageItems": [{ "id": "m1", "type": "text", "content": "Em que data você prefere? (formato DD/MM)" }] } },
|
|
303
304
|
{ "id": "n3", "type": "wait_response", "position": { "x": 690, "y": 300 }, "data": { "label": "Data", "waitTime": 30, "waitUnit": "minutes", "validation": "regex", "regexPattern": "^\\d{2}/\\d{2}$", "invalidMessage": "Formato inválido. Use DD/MM (ex: 25/05)", "maxRetries": 2, "saveTo": "variable", "saveVariable": "data_pref" } },
|
|
304
305
|
{ "id": "n4", "type": "api", "position": { "x": 1010, "y": 300 }, "data": {
|
|
@@ -332,7 +333,7 @@
|
|
|
332
333
|
```json
|
|
333
334
|
{
|
|
334
335
|
"nodes": [
|
|
335
|
-
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "label_added", "
|
|
336
|
+
{ "id": "n1", "type": "start", "position": { "x": 50, "y": 300 }, "data": { "label": "Início", "triggers": [{ "type": "label_added", "label_names": ["urgente"] }] } },
|
|
336
337
|
{ "id": "n2", "type": "action", "position": { "x": 370, "y": 300 }, "data": { "label": "Dispara webhook", "items": [
|
|
337
338
|
{ "key": "send_webhook", "config": {
|
|
338
339
|
"url": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
|
|
@@ -120,6 +120,13 @@ Glossário completo de termos, status codes, enums e conceitos da plataforma. Us
|
|
|
120
120
|
- `Channel::Telegram` — Telegram
|
|
121
121
|
- `Channel::Api` — webhook custom
|
|
122
122
|
- `Channel::Voice` — VoIP / chamadas
|
|
123
|
+
- `Channel::Sms` — SMS (Bandwidth)
|
|
124
|
+
- `Channel::TwilioSms` — SMS / WhatsApp via Twilio
|
|
125
|
+
- `Channel::Line` — LINE
|
|
126
|
+
- `Channel::Tiktok` — TikTok
|
|
127
|
+
- `Channel::TwitterProfile` — Twitter / X
|
|
128
|
+
|
|
129
|
+
> **WAHA — status de conexão:** para inbox `Channel::Waha`, o estado da sessão fica em `additional_attributes.session_status` (`WORKING`, `SCAN_QR_CODE`, `STARTING`, `STOPPED`, `FAILED`). `WORKING` = WhatsApp conectado. Exposto no serializer da inbox (`_inbox.json.jbuilder`).
|
|
123
130
|
|
|
124
131
|
### Campos importantes
|
|
125
132
|
- `id`: ID interno
|
|
@@ -198,7 +205,7 @@ Glossário completo de termos, status codes, enums e conceitos da plataforma. Us
|
|
|
198
205
|
|
|
199
206
|
## Booking (Agendamento)
|
|
200
207
|
|
|
201
|
-
- `
|
|
208
|
+
- `booking_event_type_id`: FK pro tipo de evento (ex: "Demo 30min")
|
|
202
209
|
- `attendee_email`, `attendee_name`, `attendee_phone`
|
|
203
210
|
- `start_time`, `end_time`: ISO 8601
|
|
204
211
|
- `status`: `scheduled`, `cancelled`, `completed`
|
|
@@ -276,9 +283,29 @@ Glossário completo de termos, status codes, enums e conceitos da plataforma. Us
|
|
|
276
283
|
| Motivo de Ganho/Perda do card | `kanban_config.win_reasons` / `loss_reasons` | ❌ NÃO |
|
|
277
284
|
| Atributo em TODO card | `kanban_config.global_custom_attributes` | só se não couber acima |
|
|
278
285
|
| Atributo de UM card específico | `kanban_item.custom_attributes` (jsonb direto) | já é nativo, não precisa definition |
|
|
279
|
-
| CPF, CNPJ, endereço do cliente | `
|
|
286
|
+
| CPF, RG, CNPJ, endereço, data nasc., gênero do cliente | mecanismo NATIVO cadastral: `PATCH /contacts/{id}/cadastral` (`update_cadastral`) | ❌ NÃO (tem nativo) |
|
|
280
287
|
| Tag pro contato (residencial, empresarial) | `labels` | ✅ SIM via Label |
|
|
281
288
|
| Etapa do funil | `funnel.stages` | ❌ NÃO |
|
|
282
289
|
| Tarefa interna do card | `kanban_item.checklist` ou `kanban_config.checklist_templates` | ❌ NÃO |
|
|
283
290
|
| Telefone validado WhatsApp do contato | atributos nativos WhatsApp Chat ID/JID/LID | já criados pelo WAHA automaticamente |
|
|
284
291
|
| Origem do lead (Google Ads, Meta) | `kanban_config.global_custom_attributes` (tipo list) | bom uso de custom_attribute |
|
|
292
|
+
|
|
293
|
+
## Dados cadastrais do contato (CPF/RG/CNPJ/endereço/etc.)
|
|
294
|
+
|
|
295
|
+
**IMPORTANTE:** dados cadastrais brasileiros têm mecanismo NATIVO dedicado — NÃO use `custom_attribute_definitions` (model=`contact_attribute`) pra isso.
|
|
296
|
+
|
|
297
|
+
**Endpoint nativo:** `PATCH /api/v1/accounts/{id}/contacts/{contact_id}/cadastral` (action `update_cadastral`), servido por `Contacts::CadastralAttributesService`.
|
|
298
|
+
|
|
299
|
+
**Body:**
|
|
300
|
+
```json
|
|
301
|
+
{ "cpf": "...", "rg": "...", "cnpj": "...", "passport": "...", "date_of_birth": "1990-05-15", "gender": "m",
|
|
302
|
+
"marital_status": "casado", "profession": "...", "address": { "cep": "...", "street": "...", "number": "...", "complement": "...", "neighborhood": "...", "city": "...", "state": "...", "country": "..." } }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Onde fica armazenado:** dentro de `contact.additional_attributes` — `gender` e `date_of_birth` na raiz (lidos por Meta CAPI/Google Ads); o resto em `additional_attributes.cadastral` (`cpf, rg, cnpj, passport, profession, marital_status, address`).
|
|
306
|
+
|
|
307
|
+
**⚠️ Imutabilidade (REGRA ABSOLUTA):** os campos `cpf, rg, cnpj, passport, date_of_birth, gender` são IMUTÁVEIS — só a PRIMEIRA escrita conta. Sobrescrever com valor diferente exige `force_update: true`, que **a IA e integrações NUNCA setam**. Ao receber valor diferente sem `force_update`, o serviço registra `pending_confirmations` (a IA deve perguntar ao cliente e só então chamar de novo com `force_update: true`). Valor igual ao já gravado é ignorado silenciosamente. Apenas o agente humano (via UI) passa `force_update: true` direto do form.
|
|
308
|
+
|
|
309
|
+
Campos MUTÁVEIS (atualizáveis livremente por IA/integração): `marital_status`, `profession` e todos os campos de `address`.
|
|
310
|
+
|
|
311
|
+
`custom_attribute_definitions` (model=`contact_attribute`) fica reservado a dado de NEGÓCIO genérico que não tem modelo dedicado (ex: "plano contratado", "nicho do cliente").
|