@lionchat/mcp-server 0.3.0 → 0.3.2

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.
@@ -0,0 +1,427 @@
1
+ # FlowBuilder — Guia de Design
2
+
3
+ Como criar fluxos do LionChat (FlowBuilder) que abrem certinhos no canvas, sem nodes empilhados, sem edges quebradas, sem campos inventados. Consulte sempre que for chamar `flows_create` ou `flows_update`.
4
+
5
+ ---
6
+
7
+ ## 1. Decisão de node — qual usar pra cada caso
8
+
9
+ | Cliente quer | Use o node | NÃO use |
10
+ |---|---|---|
11
+ | Mandar mensagem (texto, template WhatsApp, áudio, imagem, lista de botões) | `send_message` | `action` pra mandar mensagem |
12
+ | Esperar resposta com opções fixas (1, 2, 3) | `wait_response` com `validation: 'options'` | `condition` depois — o próprio wait_response roteia pelas opções |
13
+ | Esperar resposta livre | `wait_response` com `validation: 'any'` | - |
14
+ | Validar CPF/email/telefone | `wait_response` com `validation: 'regex'` | `condition` com regex |
15
+ | Ramificar por valor de variável/atributo | `condition` | `wait_response` |
16
+ | Atribuir agente, time, label, status, prioridade, kanban, captain | `action` com `key` específica | - |
17
+ | Chamar API externa | `api` | `action` (não tem chamada HTTP genérica) |
18
+ | Esperar X tempo | `wait` | - |
19
+ | Esperar "segunda-feira às 9h" | `wait` com `waitUnit: 'weekday'` | `condition` por horário |
20
+ | Salvar valor calculado em variável | `set_variable` | `action` |
21
+ | Gerar resposta com IA | `ai` mode `generate` | - |
22
+ | Extrair info da mensagem (ex: pegar valor numérico) | `ai` mode `extract` | regex no wait_response |
23
+ | Classificar intenção da mensagem | `ai` mode `intent` | - |
24
+ | Analisar sentimento | `ai` mode `sentiment` | - |
25
+ | Encerrar fluxo no meio | `action` com `key: 'deactivate_flow'` OU `exit_conditions` no nível do flow | - |
26
+ | Distribuir aleatório entre branches | `randomizer` | - |
27
+ | Atualizar info de grupo WhatsApp (WAHA) | `update_group` | - |
28
+ | Iniciar outro fluxo | `action` com `key: 'start_flow'` OU node `activate_flow` | - |
29
+
30
+ ---
31
+
32
+ ## 2. Schema válido por tipo de node
33
+
34
+ Todo node tem essa estrutura base:
35
+
36
+ ```json
37
+ {
38
+ "id": "uuid-ou-string-unica",
39
+ "type": "<tipo do node>",
40
+ "position": { "x": 0, "y": 0 },
41
+ "data": { "...": "campos específicos do tipo" }
42
+ }
43
+ ```
44
+
45
+ ### 2.1 `start`
46
+
47
+ ```json
48
+ {
49
+ "id": "node-start",
50
+ "type": "start",
51
+ "position": { "x": 50, "y": 300 },
52
+ "data": {
53
+ "label": "Início",
54
+ "triggers": [
55
+ { "type": "message_received", "keywords": ["oi", "ola"], "match_mode": "contains" }
56
+ ]
57
+ }
58
+ }
59
+ ```
60
+
61
+ **Triggers válidos:** `message_received`, `conversation_created`, `conversation_resolved`, `label_added`, `label_removed`, `cron`, `webhook`.
62
+
63
+ **Handles que SAEM:** `success`.
64
+
65
+ ### 2.2 `send_message`
66
+
67
+ ```json
68
+ {
69
+ "id": "node-send-1",
70
+ "type": "send_message",
71
+ "position": { "x": 370, "y": 300 },
72
+ "data": {
73
+ "label": "Boas-vindas",
74
+ "messageItems": [
75
+ { "id": "m1", "type": "text", "content": "Oi {{contact.name}}! Como posso ajudar?" },
76
+ { "id": "m2", "type": "delay", "seconds": 2 },
77
+ { "id": "m3", "type": "whatsapp_template", "templateId": 123, "params": ["valor1"] }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ **Tipos de item válidos:** `text`, `whatsapp_template` (ou `template`), `canned_response`, `user_input` (lista de botões interativos), `delay`, `attachment`, `audio`.
84
+
85
+ **ATENÇÃO:** usa `messageItems` (NÃO `items`).
86
+
87
+ **Handles que SAEM:** `success`, `no_response` (quando tem `user_input` com timeout), `error`.
88
+
89
+ ### 2.3 `wait_response`
90
+
91
+ ```json
92
+ {
93
+ "id": "node-wait-1",
94
+ "type": "wait_response",
95
+ "position": { "x": 690, "y": 300 },
96
+ "data": {
97
+ "label": "Pergunta menu",
98
+ "waitTime": 60,
99
+ "waitUnit": "minutes",
100
+ "validation": "options",
101
+ "acceptedOptions": ["1", "2", "3"],
102
+ "invalidMessage": "Por favor responda com 1, 2 ou 3",
103
+ "maxRetries": 3,
104
+ "saveTo": "attribute",
105
+ "saveAttrKey": "escolha_menu"
106
+ }
107
+ }
108
+ ```
109
+
110
+ **`validation` válidos:** `any`, `options`, `regex`.
111
+
112
+ **`saveTo` válidos:** `variable` (variável temporária do flow), `attribute` (custom_attributes da CONVERSA), `contact_attribute` (custom_attributes do CONTATO), `""` (não salva).
113
+
114
+ **Handles que SAEM dependem da validation:**
115
+ - `validation: 'any'` → `success`, `timeout`
116
+ - `validation: 'options'` → `option_<valor>` para cada valor em `acceptedOptions` (ex: `option_1`, `option_2`, `option_sim`) + `timeout`
117
+ - `validation: 'regex'` → `success`, `timeout`
118
+
119
+ **REGRA:** depois de wait_response com options, NUNCA coloque node `condition` pra ramificar — ligue os edges direto nos handles `option_X`.
120
+
121
+ ### 2.4 `condition`
122
+
123
+ ```json
124
+ {
125
+ "id": "node-cond-1",
126
+ "type": "condition",
127
+ "position": { "x": 1010, "y": 300 },
128
+ "data": {
129
+ "label": "Tipo de cliente",
130
+ "conditions": [
131
+ { "id": "c1", "label": "VIP", "field": "contact.custom_attributes.plano", "operator": "equal", "value": "premium" },
132
+ { "id": "c2", "label": "Padrão", "field": "contact.custom_attributes.plano", "operator": "equal", "value": "standard" }
133
+ ]
134
+ }
135
+ }
136
+ ```
137
+
138
+ **Operadores válidos:** `equal` (ou `field_equals`), `not_equal`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `is_present`, `is_blank`, `regex`.
139
+
140
+ **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
+
142
+ **ATENÇÃO:** o handle é `cond_INDEX` baseado na posição no array, NÃO o `id` da condição. Se você definir `conditions[0].id = "vip"`, mesmo assim o handle é `cond_0`.
143
+
144
+ ### 2.5 `action`
145
+
146
+ ```json
147
+ {
148
+ "id": "node-action-1",
149
+ "type": "action",
150
+ "position": { "x": 1330, "y": 300 },
151
+ "data": {
152
+ "label": "Finalizar atendimento",
153
+ "items": [
154
+ { "key": "assign_team", "config": { "team_id": 5 } },
155
+ { "key": "add_label", "config": { "labels": ["lead-qualificado"] } },
156
+ { "key": "create_kanban_item", "config": { "funnel_id": 3, "funnel_stage": "novo_lead" } }
157
+ ]
158
+ }
159
+ }
160
+ ```
161
+
162
+ **ATENÇÃO:** items usa `config` (NÃO `params`).
163
+
164
+ **Keys de action válidas:**
165
+
166
+ | Key | config esperado | Efeito |
167
+ |---|---|---|
168
+ | `assign_agent` | `{ agent_id }` | Atribui agente humano à conversa |
169
+ | `assign_team` | `{ team_id }` | Atribui time |
170
+ | `change_status` | `{ status: 'open' \| 'resolved' \| 'pending' \| 'snoozed' }` | Muda status da conversa |
171
+ | `change_priority` | `{ priority: 'urgent' \| 'high' \| 'medium' \| 'low' }` | Muda prioridade |
172
+ | `add_label` | `{ labels: ['slug1', 'slug2'] }` | Adiciona labels |
173
+ | `remove_label` | `{ labels: ['slug'] }` | Remove labels |
174
+ | `mute_conversation` | `{}` | Silencia notificações |
175
+ | `add_private_note` | `{ content: 'texto' }` | Adiciona nota interna |
176
+ | `create_kanban_item` | `{ funnel_id, funnel_stage, item_details? }` | Cria card no Kanban |
177
+ | `move_kanban_item_to_stage` | `{ funnel_stage }` | Move card (precisa ter card vinculado) |
178
+ | `move_kanban_stage` | `{ funnel_id, funnel_stage }` | Idem |
179
+ | `set_kanban_item_status` | `{ status: 'won' \| 'lost' \| 'active' }` | Marca status do card |
180
+ | `set_won` | `{}` | Atalho pra ganho |
181
+ | `set_lost` | `{ reason? }` | Atalho pra perdido |
182
+ | `assign_agent_card` | `{ agent_id }` | Atribui agente ao card |
183
+ | `add_card_note` | `{ content }` | Nota no card |
184
+ | `send_webhook` | `{ url, headers?, body? }` | Dispara webhook externo |
185
+ | `start_flow` | `{ flow_id }` | Inicia outro fluxo |
186
+ | `deactivate_flow` ou `disable_flow` | `{}` | Encerra fluxo atual |
187
+ | `update_attribute` | `{ entity: 'contact'\|'conversation'\|'card', key, value }` | Seta custom_attribute |
188
+ | `assign_captain` (ou `assign_captain_assistant`) | `{ assistant_id }` | Atribui IA Captain |
189
+ | `deactivate_captain` | `{}` | Tira a IA da conversa |
190
+
191
+ **Handles que SAEM:** `success`. Não tem handle `error` — falhas viram warning silencioso e o flow continua.
192
+
193
+ ### 2.6 `api`
194
+
195
+ ```json
196
+ {
197
+ "id": "node-api-1",
198
+ "type": "api",
199
+ "position": { "x": 1330, "y": 480 },
200
+ "data": {
201
+ "label": "Consulta CRM externo",
202
+ "method": "POST",
203
+ "url": "https://api.example.com/leads",
204
+ "headers": [
205
+ { "key": "Authorization", "value": "Bearer {{env.CRM_TOKEN}}" },
206
+ { "key": "Content-Type", "value": "application/json" }
207
+ ],
208
+ "body": "{\"name\":\"{{contact.name}}\",\"email\":\"{{contact.email}}\"}",
209
+ "apiResponseVar": "crm_response"
210
+ }
211
+ }
212
+ ```
213
+
214
+ **Handles que SAEM:** `success`, `error`.
215
+
216
+ ### 2.7 `ai`
217
+
218
+ ```json
219
+ {
220
+ "id": "node-ai-1",
221
+ "type": "ai",
222
+ "position": { "x": 1010, "y": 300 },
223
+ "data": {
224
+ "label": "Classifica intenção",
225
+ "aiMode": "intent",
226
+ "aiPrompt": "Classifique a intenção da última mensagem como: compra, suporte, reclamacao ou outro",
227
+ "aiIntentOptions": ["compra", "suporte", "reclamacao", "outro"],
228
+ "aiResponseVar": "intent_result"
229
+ }
230
+ }
231
+ ```
232
+
233
+ **`aiMode` válidos:** `generate`, `intent`, `sentiment`, `extract`.
234
+
235
+ **Handles que SAEM dependem do mode:**
236
+ - `generate` → `success`, `error`
237
+ - `intent` → uma saída por opção em `aiIntentOptions` (ex: `intent_compra`, `intent_suporte`) + `error`
238
+ - `sentiment` → `positive`, `negative`, `neutral` + `error`
239
+ - `extract` → `success`, `error` (resultado salvo em `aiResponseVar`)
240
+
241
+ ### 2.8 `wait`
242
+
243
+ ```json
244
+ {
245
+ "id": "node-wait-time-1",
246
+ "type": "wait",
247
+ "position": { "x": 690, "y": 300 },
248
+ "data": {
249
+ "label": "Espera 10 min",
250
+ "waitTime": 10,
251
+ "waitUnit": "minutes"
252
+ }
253
+ }
254
+ ```
255
+
256
+ **`waitUnit` válidos:** `seconds`, `minutes`, `hours`, `days`, `weekday` (espera próximo dia da semana).
257
+
258
+ Pra `weekday`, use também `targetWeekday` (0=domingo, 1=segunda... 6=sábado) e `targetHour` (0-23).
259
+
260
+ **Handles que SAEM:** `success`.
261
+
262
+ ### 2.9 `set_variable`
263
+
264
+ ```json
265
+ {
266
+ "id": "node-setvar-1",
267
+ "type": "set_variable",
268
+ "position": { "x": 370, "y": 300 },
269
+ "data": {
270
+ "label": "Define contexto",
271
+ "variables": [
272
+ { "name": "origem_lead", "value": "Facebook Ads" },
273
+ { "name": "score", "value": "{{contact.custom_attributes.lead_score}}" }
274
+ ]
275
+ }
276
+ }
277
+ ```
278
+
279
+ **Handles que SAEM:** `success`.
280
+
281
+ ### 2.10 `randomizer`
282
+
283
+ ```json
284
+ {
285
+ "id": "node-rand-1",
286
+ "type": "randomizer",
287
+ "position": { "x": 690, "y": 300 },
288
+ "data": {
289
+ "label": "A/B test mensagem",
290
+ "mode": "branches",
291
+ "branches": [
292
+ { "id": "A", "label": "Variante A", "weight": 50 },
293
+ { "id": "B", "label": "Variante B", "weight": 50 }
294
+ ]
295
+ }
296
+ }
297
+ ```
298
+
299
+ **Handles que SAEM:** o `id` de cada branch (`A`, `B`, ...). Em `mode: 'distribute_agents'` é `success`.
300
+
301
+ ### 2.11 `update_group` (WAHA apenas)
302
+
303
+ Atualiza nome/descrição/foto de grupo WhatsApp. Use só quando flow roda em inbox WAHA e a conversa for de grupo.
304
+
305
+ **Handles que SAEM:** `success`, `error`.
306
+
307
+ ### 2.12 `activate_flow`
308
+
309
+ ```json
310
+ {
311
+ "id": "node-act-1",
312
+ "type": "activate_flow",
313
+ "position": { "x": 1010, "y": 300 },
314
+ "data": { "label": "Inicia flow B", "flow_id": 42 }
315
+ }
316
+ ```
317
+
318
+ **Handles que SAEM:** `success`, `error`.
319
+
320
+ ---
321
+
322
+ ## 3. Edges
323
+
324
+ ```json
325
+ {
326
+ "id": "edge-1",
327
+ "source": "node-start",
328
+ "target": "node-send-1",
329
+ "sourceHandle": "success",
330
+ "type": "deletable",
331
+ "animated": true
332
+ }
333
+ ```
334
+
335
+ **REGRAS:**
336
+ - `sourceHandle` é OBRIGATÓRIO em todo edge
337
+ - Tem que casar com um handle que o node `source` EFETIVAMENTE EXPÕE (ver tabelas acima)
338
+ - `id`: usar formato `edge-N` ou `e<source>-<target>`
339
+ - `type: 'deletable'` e `animated: true` são os defaults visuais
340
+
341
+ **Handles INVENTADOS quebram o flow.** Exemplos do que NÃO usar:
342
+ - `c1`, `c2`, `c3` (use `cond_0`, `cond_1`)
343
+ - `branch1`, `branchA` (no condition; use `cond_X`)
344
+ - `out`, `output`, `next` (use o nome real do handle)
345
+ - `option1` sem underscore (use `option_1`)
346
+
347
+ ---
348
+
349
+ ## 4. Layout e positioning
350
+
351
+ Sem `position`, todos os nodes empilham no (0,0). Sempre informe.
352
+
353
+ ### Regras de espaçamento
354
+
355
+ | Direção | Valor |
356
+ |---|---|
357
+ | Espaço horizontal entre nodes sequenciais | **+320** em X |
358
+ | Espaço vertical entre branches paralelos | **+150** em Y (2 ramos), **+180** (3 ramos), **+150** cada (4+) |
359
+ | Y do node start | **300** (ponto médio) |
360
+
361
+ ### Exemplo de layout
362
+
363
+ Fluxo linear de 4 nodes:
364
+ ```
365
+ (50, 300) → (370, 300) → (690, 300) → (1010, 300)
366
+ ```
367
+
368
+ Fluxo com 1 wait_response 3 opções:
369
+ ```
370
+ (1330, 120) -> option_1
371
+ ↑
372
+ (50,300) → (370,300) → (690,300) → wait → (1330, 300) -> option_2
373
+ ↓
374
+ (1330, 480) -> option_3
375
+ ```
376
+
377
+ Cada branch continua em seu próprio Y, X avança normal:
378
+ ```
379
+ option_1 path: (1330, 120) → (1650, 120) → (1970, 120)
380
+ option_2 path: (1330, 300) → (1650, 300) → (1970, 300)
381
+ option_3 path: (1330, 480) → (1650, 480) → (1970, 480)
382
+ ```
383
+
384
+ ### Regra importante
385
+
386
+ Nodes nunca devem ficar com a mesma coordenada `(x, y)`. Se dois nodes têm posição idêntica, eles SOBREPÕEM visualmente no canvas.
387
+
388
+ ---
389
+
390
+ ## 5. Erros comuns (lista negra)
391
+
392
+ | Erro | Por que quebra | Forma certa |
393
+ |---|---|---|
394
+ | `items` no send_message | Schema usa `messageItems` | `messageItems: [...]` |
395
+ | `items[].params` no action | Schema usa `config` | `items: [{ key, config: {...} }]` |
396
+ | Falta `position` em algum node | Nodes empilham no canvas | Sempre `position: {x, y}` |
397
+ | `sourceHandle: "c1"` ou `"vip"` no condition | Não existem | `"cond_0"`, `"cond_1"`, etc |
398
+ | Node `condition` depois de `wait_response` com options | Redundante e atrapalha | Ligue `option_X` direto no próximo node |
399
+ | Edge sem `sourceHandle` | Frontend não consegue rotear | Sempre informe |
400
+ | Edge com `target` apontando pra ID que não existe | Quebra o grafo | Confira que `target` está em `nodes[]` |
401
+ | `channel_type: "WhatsApp"` | Precisa do nome de classe Rails | `"Channel::Whatsapp"`, `"Channel::Waha"`, `"Channel::WebWidget"` |
402
+ | `validation: "option"` (singular) | Não existe | `"options"` (plural) |
403
+ | `waitTime: "60"` (string) | Espera Integer | `waitTime: 60` |
404
+ | `inbox_ids` aninhado em `{flow:{...}}` | No MCP, achata-se sozinho | Passa `inbox_ids: [1, 2]` no nível raiz |
405
+ | Mais de 1 node `start` | Flow precisa ter exatamente 1 ponto de entrada | Só 1 |
406
+ | Node sem edge entrando (exceto start) | Node nunca executa | Confira que todo node não-start tem ao menos 1 edge target apontando pra ele |
407
+ | `flow_data` sem `nodes` ou sem `edges` | Estrutura inválida | Inclua sempre, mesmo que `edges: []` |
408
+
409
+ ---
410
+
411
+ ## 6. Checklist pre-submit
412
+
413
+ Antes de chamar `flows_create` ou `flows_update`, valide mentalmente:
414
+
415
+ - [ ] Existe exatamente 1 node `type: 'start'`
416
+ - [ ] Todo node tem `id` único
417
+ - [ ] Todo node tem `type` válido (lista da seção 1)
418
+ - [ ] Todo node tem `position: { x: <int>, y: <int> }`
419
+ - [ ] Nenhum par de nodes tem a mesma posição `(x, y)`
420
+ - [ ] Todo node tem `data` com campos do schema do tipo
421
+ - [ ] Todo node não-start tem pelo menos 1 edge chegando (`edge.target = node.id`)
422
+ - [ ] Todo edge tem `source`, `target` e `sourceHandle`
423
+ - [ ] Todo `sourceHandle` é um handle real exposto pelo node source (seção 2)
424
+ - [ ] `channel_type` é classe Rails (`Channel::Waha` etc)
425
+ - [ ] `inbox_ids` (se enviado) tem inboxes do mesmo `channel_type`
426
+ - [ ] Não tem `condition` redundante depois de `wait_response` com options
427
+ - [ ] Layout: nodes em ordem visual da esquerda pra direita, sem sobreposição