chatgraph 1.2.1__tar.gz → 1.2.3__tar.gz

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.
Files changed (65) hide show
  1. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/skills/chatgraph-framework/SKILL.md +528 -506
  2. {chatgraph-1.2.1 → chatgraph-1.2.3}/PKG-INFO +1 -1
  3. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/__init__.py +2 -0
  4. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/default_guard.py +10 -1
  5. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/__init__.py +1 -0
  6. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/user_logger.py +18 -0
  7. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/messages/message_consumer.py +70 -48
  8. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/services/router_http_client.py +106 -28
  9. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/usercall.py +126 -23
  10. chatgraph-1.2.3/example.py +98 -0
  11. {chatgraph-1.2.1 → chatgraph-1.2.3}/pyproject.toml +80 -80
  12. chatgraph-1.2.3/tests/unit/test_message_consumer.py +364 -0
  13. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_router_http_client.py +209 -10
  14. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_user_logger.py +35 -1
  15. chatgraph-1.2.3/tests/unit/test_usercall.py +400 -0
  16. {chatgraph-1.2.1 → chatgraph-1.2.3}/uv.lock +1139 -1139
  17. chatgraph-1.2.1/example.py +0 -127
  18. chatgraph-1.2.1/tests/unit/test_usercall.py +0 -75
  19. {chatgraph-1.2.1 → chatgraph-1.2.3}/.env.example +0 -0
  20. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/SkillManager.agent.md +0 -0
  21. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/architect.agent.md +0 -0
  22. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/code-reviewer.agent.md +0 -0
  23. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/developer.agent.md +0 -0
  24. {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/copilot-instructions.md +0 -0
  25. {chatgraph-1.2.1 → chatgraph-1.2.3}/.gitignore +0 -0
  26. {chatgraph-1.2.1 → chatgraph-1.2.3}/.python-version +0 -0
  27. {chatgraph-1.2.1 → chatgraph-1.2.3}/LICENSE +0 -0
  28. {chatgraph-1.2.1 → chatgraph-1.2.3}/README.md +0 -0
  29. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/auth/credentials.py +0 -0
  30. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/chatbot_model.py +0 -0
  31. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/chatbot_router.py +0 -0
  32. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/default_functions.py +0 -0
  33. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/cli/__init__.py +0 -0
  34. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/container/container.py +0 -0
  35. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/error/chatbot_error.py +0 -0
  36. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/error/route_error.py +0 -0
  37. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/gRPC/gRPCCall.py +0 -0
  38. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/logger.py +0 -0
  39. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/actions.py +0 -0
  40. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/http_responses.py +0 -0
  41. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/message.py +0 -0
  42. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/userstate.py +0 -0
  43. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router.proto +0 -0
  44. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router_pb2.py +0 -0
  45. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router_pb2_grpc.py +0 -0
  46. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/services/__init__.py +0 -0
  47. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/background_task.py +0 -0
  48. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/end_types.py +0 -0
  49. {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/route.py +0 -0
  50. {chatgraph-1.2.1 → chatgraph-1.2.3}/docs/gaps-go-to-python.md +0 -0
  51. {chatgraph-1.2.1 → chatgraph-1.2.3}/docs/gaps-python-to-go.md +0 -0
  52. {chatgraph-1.2.1 → chatgraph-1.2.3}/example2.py +0 -0
  53. {chatgraph-1.2.1 → chatgraph-1.2.3}/jsons/voll_return.json +0 -0
  54. {chatgraph-1.2.1 → chatgraph-1.2.3}/poetry.lock +0 -0
  55. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/__init__.py +0 -0
  56. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/__init__.py +0 -0
  57. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/conftest.py +0 -0
  58. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/test_router_client_integration.py +0 -0
  59. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/__init__.py +0 -0
  60. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/conftest.py +0 -0
  61. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_default_guard.py +0 -0
  62. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_guard.py +0 -0
  63. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_models_actions.py +0 -0
  64. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_models_message.py +0 -0
  65. {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_models_userstate.py +0 -0
@@ -1,506 +1,528 @@
1
- ---
2
- name: chatgraph-framework
3
- description: "Use when: implementing a chatbot with chatgraph, creating routes with @app.route or @router.route, using UserCall, Route, Message, File, Button, EndChatResponse, RedirectResponse, TransferToMenu, TransferToHuman, BackgroundTask, setting up ChatbotApp, ChatbotRouter, include_router, configuring RabbitMQ consumer, adding per-user file logging with UserLoggerManager, integrating chatgraph in a new Python project."
4
- argument-hint: "Descreva o fluxo de rotas que deseja implementar (opcional)"
5
- ---
6
-
7
- # chatgraph-framework
8
-
9
- Guia completo para implementar o framework **chatgraph** em projetos Python. Cobre setup, rotas, tipos de resposta, mensagens, logging e modularização.
10
-
11
- ## Visão Geral
12
-
13
- O chatgraph é um framework de chatbot que consome mensagens via RabbitMQ e despacha para funções de rota registradas via decorador. O ponto de entrada de toda rota é `UserCall`, que agrega mensagem recebida, estado do usuário e cliente HTTP.
14
-
15
- ```
16
- RabbitMQ → MessageConsumer → ChatbotApp.process_message() → @route() → UserCall
17
- ```
18
-
19
- ---
20
-
21
- ## 1. Instalação e Dependências
22
-
23
- ```toml
24
- # pyproject.toml
25
- [project]
26
- requires-python = ">=3.12"
27
- dependencies = [
28
- "chatgraph",
29
- "python-dotenv",
30
- ]
31
- ```
32
-
33
- ```bash
34
- pip install chatgraph python-dotenv
35
- ```
36
-
37
- ---
38
-
39
- ## 2. Variáveis de Ambiente Obrigatórias
40
-
41
- ```env
42
- # .env
43
- RABBIT_USER=guest
44
- RABBIT_PASS=guest
45
- RABBIT_URI=localhost:5672
46
- RABBIT_QUEUE=minha_fila
47
- RABBIT_PREFETCH=1
48
- RABBIT_VHOST=/
49
- ROUTER_URL=http://localhost:8000
50
- ROUTER_TOKEN=meu_token
51
-
52
- # Opcional — nível de log padrão (DEBUG, INFO, WARNING, ERROR). Default: INFO
53
- CHATGRAPH_LOG_LEVEL=INFO
54
- ```
55
-
56
- ---
57
-
58
- ## 3. Setup Mínimo
59
-
60
- ```python
61
- from chatgraph import ChatbotApp, UserCall, Route
62
- from chatgraph.logger import get_system_logger
63
- from dotenv import load_dotenv
64
-
65
- load_dotenv()
66
-
67
- _logger = get_system_logger() # logs de módulo → chatgraph_logs/system.log
68
- _logger.info('Aplicação inicializada')
69
-
70
- app = ChatbotApp()
71
- # Opções disponíveis:
72
- # app = ChatbotApp(
73
- # log_level='DEBUG', # sobrescreve CHATGRAPH_LOG_LEVEL
74
- # guard=meu_guard_customizado, # substitui o default_guard
75
- # )
76
-
77
- @app.route('start')
78
- async def start(usercall: UserCall, rota: Route):
79
- usercall.logger.info('Usuário na rota start')
80
- await usercall.send('Olá! Como posso ajudar?')
81
-
82
- app.start()
83
- ```
84
-
85
- > `app.start()` inicia o loop assíncrono de consumo do RabbitMQ. É bloqueante.
86
-
87
- ---
88
-
89
- ## 4. Parâmetros das Funções de Rota
90
-
91
- Toda função de rota pode declarar **qualquer combinação** dos parâmetros abaixo (ordem livre — o framework resolve por tipo):
92
-
93
- | Parâmetro | Tipo | Descrição |
94
- |-----------|------|-----------|
95
- | `usercall` | `UserCall` | Mensagem + estado + cliente HTTP do usuário atual |
96
- | `rota` | `Route` | Rota atual e histórico de navegação |
97
-
98
- ```python
99
- @app.route('start')
100
- async def start(usercall: UserCall, rota: Route): ...
101
-
102
- @app.route('start')
103
- async def start(usercall: UserCall): ... # Route é opcional
104
-
105
- @app.route('start')
106
- async def start(rota: Route): ... # UserCall é opcional
107
- ```
108
-
109
- ---
110
-
111
- ## 5. UserCall — API Completa
112
-
113
- ```python
114
- # Dados do usuário
115
- usercall.user_id # str — ID do usuário
116
- usercall.company_id # str — ID da empresa
117
- usercall.content_message # str — texto da mensagem recebida
118
- usercall.menu # Menu — menu atual do usuário
119
- usercall.route # str — rota atual (ex: "start.choice")
120
- usercall.observation # dict — observações da sessão (get/set)
121
- usercall.chatID # ChatID(user_id, company_id)
122
- usercall.user # User — dados completos do usuário da sessão
123
-
124
- # Acesso aos sub-objetos de usercall.user
125
- usercall.user.data.name # str | None — nome
126
- usercall.user.data.cpf # str | None — CPF
127
- usercall.user.data.phone # str | None — telefone
128
- usercall.user.data.email # str | None — e-mail
129
- usercall.user.data.nickname # str | None — apelido
130
- usercall.user.data.profile_photo_url # str | None — foto de perfil
131
- usercall.user.data.account # str | None — conta/matrícula
132
- usercall.user.data.birth_date # str | None — data de nascimento
133
-
134
- usercall.user.identity.auth_level # AuthLevel — nível de autenticação (BLOCKED/UNKNOWN/READ/WRITE)
135
- usercall.user.identity.cpf # str | None — CPF autenticado
136
- usercall.user.identity.active # bool | None — usuário ativo
137
- usercall.user.identity.auth_status # str | None — status de autenticação
138
- usercall.user.identity.device_id # str | None — ID do dispositivo
139
-
140
- usercall.user.internal # UserInternal | None — dados internos (RH)
141
- usercall.user.internal.matricula # str | None
142
- usercall.user.internal.cargo # str | None
143
- usercall.user.internal.filial # str | None
144
- usercall.user.internal.empresa # str | None
145
- usercall.user.internal.data_admissao # str | None
146
-
147
- # Logging contextualizado por usuário
148
- usercall.logger # logging.Logger → chatgraph_logs/{user_id}_{company_id}.log
149
-
150
- # Envio de mensagens
151
- await usercall.send(message) # Message | File | str | int | float
152
-
153
- # Navegação e estado
154
- await usercall.set_route('nova_rota')
155
- await usercall.set_observation('texto')
156
- await usercall.add_observation({'chave': 'valor'})
157
- await usercall.update_user_data(user)
158
-
159
- # Encerramento
160
- await usercall.end_chat(end_action_id='id', end_action_name='nome', observation='...')
161
- await usercall.transfer_to_menu('nome_menu', 'mensagem_usuario')
162
- await usercall.transfer_to_menu('nome_menu', 'mensagem_usuario', route='rota_inicial') # com rota de entrada
163
-
164
- # Consulta
165
- menu = await usercall.get_menu(name='nome_menu') # por nome
166
- menu = await usercall.get_menu(menu_id=42) # por ID
167
- menu = await usercall.get_menu(description='Suporte TI') # por descrição
168
-
169
- # Setters diretos (síncronos — não persistem imediatamente, usam loop existente)
170
- usercall.observation = {'chave': 'valor'} # dict — substitui toda a observação
171
- usercall.content_message = 'nova mensagem' # str — sobrescreve o texto recebido
172
- ```
173
-
174
- ---
175
-
176
- ## 6. Tipos de Retorno de Rota
177
-
178
- Cada função de rota deve retornar **um dos tipos** abaixo:
179
-
180
- ### `RedirectResponse(route)` — Redireciona e re-executa imediatamente
181
- ```python
182
- return RedirectResponse('choice_start')
183
- ```
184
-
185
- ### `Route(current_node)` — Atualiza a rota e aguarda nova mensagem
186
- ```python
187
- return Route('aguardando_resposta')
188
- ```
189
-
190
- ### `EndChatResponse(end_chat_id, end_chat_name?, observations?)` — Encerra o chat
191
- ```python
192
- return EndChatResponse('voll_ended')
193
- return EndChatResponse('', end_chat_name='Encerrado pelo usuário', observations='motivo')
194
- ```
195
-
196
- ### `TransferToMenu(menu, user_message, route?)` — Transfere para outro menu
197
- ```python
198
- return TransferToMenu('p0299_suporte_ti', 'Transferindo...')
199
- return TransferToMenu('p0299_suporte_ti', 'Transferindo...', route='etapa_inicial') # inicia em rota específica
200
- ```
201
-
202
- ### `TransferToHuman(campaign_id?, campaign_name?, observations?)` — Transfere para humano
203
- ```python
204
- return TransferToHuman(campaign_name='Suporte N2')
205
- ```
206
-
207
- ### `BackgroundTask(async_func, *args, **kwargs)` — Executa tarefa em background e encadeia o retorno
208
- ```python
209
- async def processar(usercall: UserCall):
210
- await usercall.send('Processando...')
211
- return EndChatResponse('concluido')
212
-
213
- return BackgroundTask(processar, usercall)
214
- ```
215
-
216
- ### Lista/tupla — Múltiplas respostas sequenciais
217
- ```python
218
- return [
219
- Message('Primeira mensagem'),
220
- Message('Segunda mensagem'),
221
- RedirectResponse('proxima_rota'),
222
- ]
223
- ```
224
-
225
- ---
226
-
227
- ## 7. Mensagens
228
-
229
- ```python
230
- from chatgraph import Message, Button, File, TextMessage, SendType
231
-
232
- # Texto simples
233
- await usercall.send('Olá!')
234
- await usercall.send(Message('Olá!'))
235
-
236
- # Com botões
237
- msg = Message(
238
- 'Escolha uma opção:',
239
- buttons=[
240
- Button('Opção 1'), # POSTBACK simples
241
- Button('Ver mais', detail='payload_123'), # com payload
242
- Button('Cancelar'),
243
- ],
244
- )
245
- await usercall.send(msg)
246
-
247
- # Arquivo por path local
248
- file = File.from_path('caminho/para/imagem.png')
249
- await usercall.send(file)
250
-
251
- # Arquivo em mensagem
252
- msg_com_arquivo = Message(file=file)
253
- await usercall.send(msg_com_arquivo)
254
- ```
255
-
256
- ### `Button` — campos
257
- ```python
258
- from chatgraph import Button
259
- from chatgraph.models.message import ButtonType # não exportado no __init__ top-level
260
-
261
- Button(
262
- title='Texto do botão', # exibido para o usuário
263
- detail='payload', # dados enviados ao pressionar (opcional)
264
- type=ButtonType.POSTBACK, # ButtonType.POSTBACK (padrão) | ButtonType.URL
265
- )
266
- ```
267
-
268
- ### `TextMessage` — dataclass
269
- ```python
270
- from chatgraph import TextMessage
271
-
272
- # Normalmente criado automaticamente por Message(str)
273
- # Acesso direto ao conteúdo recebido:
274
- usercall.content_message # equivale a mensagem_recebida.text_message.detail
275
- ```
276
-
277
- ### `SendType` enum para arquivos
278
- ```python
279
- from chatgraph import SendType
280
-
281
- # SendType.IMAGE | SendType.VIDEO | SendType.AUDIO | SendType.FILE | SendType.UNKNOWN
282
- file = File.from_path('video.mp4')
283
- file.send_type = SendType.VIDEO
284
- ```
285
-
286
- ---
287
-
288
- ## 8. Logging
289
-
290
- ### Níveis recomendados
291
-
292
- | Situação | Método | Destino |
293
- |----------|--------|---------|
294
- | Dentro de rota, com `usercall` | `usercall.logger.info/debug/warning/error(...)` | `chatgraph_logs/{user_id}_{company_id}.log` |
295
- | Fora de rota (startup, módulo) | `_logger = get_system_logger()` | `chatgraph_logs/system.log` |
296
-
297
- ```python
298
- from chatgraph.logger import get_system_logger, set_level
299
-
300
- _logger = get_system_logger()
301
- _logger.info('App iniciada')
302
-
303
- # Alterar nível de log em runtime (afeta todos os loggers existentes)
304
- set_level('DEBUG') # ou logging.DEBUG
305
- # Equivalente: UserLoggerManager.set_level('DEBUG')
306
-
307
- @app.route('start')
308
- async def start(usercall: UserCall):
309
- usercall.logger.info('Usuário entrou em start')
310
- usercall.logger.debug(f'Mensagem recebida: {usercall.content_message}')
311
- try:
312
- await usercall.send('Olá!')
313
- except Exception as e:
314
- usercall.logger.error(f'Erro ao enviar: {e}')
315
- ```
316
-
317
- ### Formato do log
318
- ```
319
- 2026-05-06 15:13:15,828 | INFO | Mensagem | nome_funcao | user123_empresa456
320
- ```
321
-
322
- ---
323
-
324
- ## 9. Modularização com `ChatbotRouter`
325
-
326
- Para projetos maiores, organize rotas em módulos separados:
327
-
328
- ```python
329
- # rotas/suporte.py
330
- from chatgraph import ChatbotRouter, UserCall, Route, RedirectResponse
331
-
332
- router = ChatbotRouter()
333
-
334
- @router.route('suporte')
335
- async def suporte(usercall: UserCall):
336
- await usercall.send('Como posso ajudar?')
337
- return Route('aguardar_resposta_suporte')
338
-
339
- @router.route('aguardar_resposta_suporte')
340
- async def aguardar(usercall: UserCall):
341
- await usercall.send(f'Você disse: {usercall.content_message}')
342
- return RedirectResponse('start')
343
- ```
344
-
345
- ```python
346
- # main.py
347
- from chatgraph import ChatbotApp
348
- from rotas.suporte import router as suporte_router
349
-
350
- app = ChatbotApp()
351
- app.include_router(suporte_router)
352
-
353
- app.start()
354
- ```
355
-
356
- > Não prefixo automático — o nome de cada `@router.route('nome')` é o nome final da rota.
357
-
358
- `ChatbotRouter` também pode absorver outro `ChatbotRouter` com `include_router()`:
359
-
360
- ```python
361
- # rotas/geral.py
362
- from chatgraph import ChatbotRouter
363
- from rotas.suporte import router as suporte_router
364
- from rotas.vendas import router as vendas_router
365
-
366
- router = ChatbotRouter()
367
- router.include_router(suporte_router)
368
- router.include_router(vendas_router)
369
- ```
370
-
371
- ---
372
-
373
- ## 10. Funções Padrão (`default_functions`)
374
-
375
- O `ChatbotApp` intercepta mensagens **antes** de despachar para a rota quando o texto corresponder a um padrão regex registrado em `default_functions`. Após a execução da função padrão, `content_message` é zerado.
376
-
377
- ### Comportamento embutido — `voltar`
378
-
379
- Por padrão, qualquer mensagem que corresponda a `^\s*(voltar)\s*$` (case-insensitive) é interceptada e executa `voltar()`, que redireciona para o nó anterior via `route.get_previous()`.
380
-
381
- ```python
382
- # Comportamento automático — nenhuma rota necessária
383
- # Usuário digita "voltar" → retorna para a rota anterior
384
- ```
385
-
386
- ### Customizar ou desabilitar as funções padrão
387
-
388
- ```python
389
- from chatgraph import ChatbotApp
390
-
391
- # Desabilitar o voltar
392
- app = ChatbotApp(default_functions={})
393
-
394
- # Adicionar função customizada
395
- from chatgraph import UserCall, Route, RedirectResponse
396
-
397
- async def ajuda(route: Route, usercall: UserCall):
398
- await usercall.send('Comandos disponíveis: voltar, sair')
399
- return RedirectResponse(route.current_node)
400
-
401
- app = ChatbotApp(default_functions={
402
- r'^\s*(voltar)\s*$': voltar, # manter o padrão
403
- r'^\s*(ajuda|help)\s*$': ajuda, # adicionar novo
404
- })
405
- ```
406
-
407
- > As funções padrão recebem `(route: Route, usercall: UserCall)` e têm acesso às mesmas respostas de rota. `auth_level` não é verificado para funções padrão.
408
-
409
- ---
410
-
411
- ## 11. Fluxo de Navegação
412
-
413
- - Rota inicial obrigatória: `start`
414
- - Sub-rotas usam notação de ponto internamente: `start.choice.confirm`
415
- - `Route(node)` adiciona o nó ao caminho atual e aguarda nova mensagem
416
- - `RedirectResponse(route)` troca para o nó e re-executa imediatamente (sem aguardar)
417
- - `rota.current_node` último segmento (ex: `"confirm"`)
418
- - `rota.previous` → rota anterior no histórico
419
- - `rota.get_next('sub_rota')` → constrói o próximo `Route` e valida se existe na lista de rotas disponíveis
420
-
421
- ---
422
-
423
- ## 12. Controle de Acesso — `auth_level` e `guard`
424
-
425
- Cada rota pode declarar um `auth_level` que é validado pelo guard antes de executar a função.
426
-
427
- ```python
428
- # Níveis suportados pelo default_guard:
429
- # 'read' → usercall.user.identity.auth_level >= AuthLevel.READ
430
- # 'write' → usercall.user.identity.auth_level >= AuthLevel.WRITE
431
- # 'internal' → usercall.user.internal não pode ser None
432
-
433
- @app.route('area_restrita', auth_level='internal')
434
- async def area_restrita(usercall: UserCall):
435
- await usercall.send('Acesso autorizado!')
436
- return RedirectResponse('menu_principal')
437
-
438
- @app.route('dados_sensiveis', auth_level='write')
439
- async def dados_sensiveis(usercall: UserCall):
440
- await usercall.send('Você tem permissão de escrita.')
441
- return EndChatResponse('concluido')
442
- ```
443
-
444
- Quando o acesso é negado, o `default_guard` redireciona para `menu_id_positiva` e salva `pending_route`, `pending_menu` e `pending_auth_level` na observação da sessão.
445
-
446
- ### Guard customizado
447
-
448
- ```python
449
- from chatgraph import ChatbotApp, UserCall, default_guard
450
- from chatgraph.types.end_types import TransferToMenu
451
-
452
- async def meu_guard(usercall: UserCall, auth_level: str) -> TransferToMenu | None:
453
- if auth_level == 'admin' and usercall.user.data.email != 'admin@empresa.com':
454
- return TransferToMenu('menu_acesso_negado', '')
455
- return None # None = acesso liberado
456
-
457
- app = ChatbotApp(guard=meu_guard)
458
- ```
459
-
460
- > Se o guard retornar `None`, a rota é executada normalmente. Qualquer outro tipo de retorno é processado como resposta de rota (ex: `TransferToMenu`, `RedirectResponse`).
461
-
462
- ---
463
-
464
- ## 13. Exemplo Completo
465
-
466
- ```python
467
- from chatgraph import (
468
- ChatbotApp, UserCall, Route,
469
- Message, Button, File,
470
- EndChatResponse, RedirectResponse, TransferToMenu,
471
- )
472
- from chatgraph.logger import get_system_logger
473
- from dotenv import load_dotenv
474
-
475
- load_dotenv()
476
- _logger = get_system_logger()
477
- _logger.info('Aplicação iniciada')
478
- app = ChatbotApp()
479
-
480
-
481
- @app.route('start')
482
- async def start(usercall: UserCall, rota: Route):
483
- usercall.logger.info('Usuário entrou em start')
484
- await usercall.send(
485
- Message('Bem-vindo! Escolha:', buttons=[Button('Suporte'), Button('Sair')])
486
- )
487
- return Route('aguardar_escolha')
488
-
489
-
490
- @app.route('aguardar_escolha')
491
- async def aguardar_escolha(usercall: UserCall):
492
- resposta = usercall.content_message
493
- usercall.logger.info(f'Escolha recebida: {resposta}')
494
-
495
- if resposta == 'Suporte':
496
- return TransferToMenu('menu_suporte', 'Transferindo para suporte...')
497
- elif resposta == 'Sair':
498
- return EndChatResponse('encerrado')
499
- else:
500
- usercall.logger.warning(f'Opção inválida: {resposta}')
501
- await usercall.send('Opção inválida. Tente novamente.')
502
- return RedirectResponse('start')
503
-
504
-
505
- app.start()
506
- ```
1
+ ---
2
+ name: chatgraph-framework
3
+ description: "Use when: implementing a chatbot with chatgraph, creating routes with @app.route or @router.route, using UserCall, Route, Message, File, Button, EndChatResponse, RedirectResponse, TransferToMenu, TransferToHuman, BackgroundTask, setting up ChatbotApp, ChatbotRouter, include_router, configuring RabbitMQ consumer, adding per-user file logging with UserLoggerManager, integrating chatgraph in a new Python project."
4
+ argument-hint: "Descreva o fluxo de rotas que deseja implementar (opcional)"
5
+ ---
6
+
7
+ # chatgraph-framework
8
+
9
+ Guia completo para implementar o framework **chatgraph** em projetos Python. Cobre setup, rotas, tipos de resposta, mensagens, logging e modularização.
10
+
11
+ ## Visão Geral
12
+
13
+ O chatgraph é um framework de chatbot que consome mensagens via RabbitMQ e despacha para funções de rota registradas via decorador. O ponto de entrada de toda rota é `UserCall`, que agrega mensagem recebida, estado do usuário e cliente HTTP.
14
+
15
+ ```
16
+ RabbitMQ → MessageConsumer → ChatbotApp.process_message() → @route() → UserCall
17
+ ```
18
+
19
+ ---
20
+
21
+ ## 1. Instalação e Dependências
22
+
23
+ ```toml
24
+ # pyproject.toml
25
+ [project]
26
+ requires-python = ">=3.12"
27
+ dependencies = [
28
+ "chatgraph",
29
+ "python-dotenv",
30
+ ]
31
+ ```
32
+
33
+ ```bash
34
+ pip install chatgraph python-dotenv
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 2. Variáveis de Ambiente Obrigatórias
40
+
41
+ ```env
42
+ # .env
43
+ RABBIT_USER=guest
44
+ RABBIT_PASS=guest
45
+ RABBIT_URI=localhost:5672
46
+ RABBIT_QUEUE=minha_fila
47
+ RABBIT_PREFETCH=1
48
+ RABBIT_VHOST=/
49
+ ROUTER_URL=http://localhost:8000
50
+ ROUTER_TOKEN=meu_token
51
+
52
+ # Opcional — nível de log padrão (DEBUG, INFO, WARNING, ERROR). Default: INFO
53
+ CHATGRAPH_LOG_LEVEL=INFO
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 3. Setup Mínimo
59
+
60
+ ```python
61
+ from chatgraph import ChatbotApp, UserCall, Route
62
+ from chatgraph.logger import get_system_logger
63
+ from dotenv import load_dotenv
64
+
65
+ load_dotenv()
66
+
67
+ _logger = get_system_logger() # logs de módulo → chatgraph_logs/system.log
68
+ _logger.info('Aplicação inicializada')
69
+
70
+ app = ChatbotApp()
71
+ # Opções disponíveis:
72
+ # app = ChatbotApp(
73
+ # log_level='DEBUG', # sobrescreve CHATGRAPH_LOG_LEVEL
74
+ # guard=meu_guard_customizado, # substitui o default_guard
75
+ # )
76
+
77
+ @app.route('start')
78
+ async def start(usercall: UserCall, rota: Route):
79
+ usercall.logger.info('Usuário na rota start')
80
+ await usercall.send('Olá! Como posso ajudar?')
81
+
82
+ app.start()
83
+ ```
84
+
85
+ > `app.start()` inicia o loop assíncrono de consumo do RabbitMQ. É bloqueante.
86
+
87
+ ---
88
+
89
+ ## 4. Parâmetros das Funções de Rota
90
+
91
+ Toda função de rota pode declarar **qualquer combinação** dos parâmetros abaixo (ordem livre — o framework resolve por tipo):
92
+
93
+ | Parâmetro | Tipo | Descrição |
94
+ |-----------|------|-----------|
95
+ | `usercall` | `UserCall` | Mensagem + estado + cliente HTTP do usuário atual |
96
+ | `rota` | `Route` | Rota atual e histórico de navegação |
97
+
98
+ ```python
99
+ @app.route('start')
100
+ async def start(usercall: UserCall, rota: Route): ...
101
+
102
+ @app.route('start')
103
+ async def start(usercall: UserCall): ... # Route é opcional
104
+
105
+ @app.route('start')
106
+ async def start(rota: Route): ... # UserCall é opcional
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 5. UserCall — API Completa
112
+
113
+ ```python
114
+ # Dados do usuário
115
+ usercall.user_id # str — ID do usuário
116
+ usercall.company_id # str — ID da empresa
117
+ usercall.content_message # str — texto da mensagem recebida
118
+ usercall.menu # Menu — menu atual do usuário
119
+ usercall.route # str — rota atual (ex: "start.choice")
120
+ usercall.observation # dict — observações da sessão (get/set)
121
+ usercall.chatID # ChatID(user_id, company_id)
122
+ usercall.user # User — dados completos do usuário da sessão
123
+
124
+ # Acesso aos sub-objetos de usercall.user
125
+ usercall.user.data.name # str | None — nome
126
+ usercall.user.data.cpf # str | None — CPF
127
+ usercall.user.data.phone # str | None — telefone
128
+ usercall.user.data.email # str | None — e-mail
129
+ usercall.user.data.nickname # str | None — apelido
130
+ usercall.user.data.profile_photo_url # str | None — foto de perfil
131
+ usercall.user.data.account # str | None — conta/matrícula
132
+ usercall.user.data.birth_date # str | None — data de nascimento
133
+
134
+ usercall.user.identity.auth_level # AuthLevel — nível de autenticação (BLOCKED/UNKNOWN/READ/WRITE)
135
+ usercall.user.identity.cpf # str | None — CPF autenticado
136
+ usercall.user.identity.active # bool | None — usuário ativo
137
+ usercall.user.identity.auth_status # str | None — status de autenticação
138
+ usercall.user.identity.device_id # str | None — ID do dispositivo
139
+
140
+ usercall.user.internal # UserInternal | None — dados internos (RH)
141
+ usercall.user.internal.matricula # str | None
142
+ usercall.user.internal.cargo # str | None
143
+ usercall.user.internal.filial # str | None
144
+ usercall.user.internal.empresa # str | None
145
+ usercall.user.internal.data_admissao # str | None
146
+
147
+ # Logging contextualizado por usuário
148
+ usercall.logger # logging.Logger → chatgraph_logs/{user_id}_{company_id}.log
149
+
150
+ # Envio de mensagens
151
+ await usercall.send(message) # Message | File | str | int | float
152
+
153
+ # Navegação e estado
154
+ await usercall.set_route('nova_rota')
155
+ await usercall.set_observation('texto')
156
+ await usercall.add_observation({'chave': 'valor'})
157
+ await usercall.update_user_data(user)
158
+
159
+ # Encerramento
160
+ await usercall.end_chat(end_action_id='id', end_action_name='nome', observation='...')
161
+ await usercall.transfer_to_menu('nome_menu', 'mensagem_usuario')
162
+ await usercall.transfer_to_menu('nome_menu', 'mensagem_usuario', route='rota_inicial') # com rota de entrada
163
+
164
+ # Consulta
165
+ menu = await usercall.get_menu(name='nome_menu') # por nome
166
+ menu = await usercall.get_menu(menu_id=42) # por ID
167
+ menu = await usercall.get_menu(description='Suporte TI') # por descrição
168
+
169
+ # Setters diretos (síncronos — não persistem imediatamente, usam loop existente)
170
+ usercall.observation = {'chave': 'valor'} # dict — substitui toda a observação
171
+ usercall.content_message = 'nova mensagem' # str — sobrescreve o texto recebido
172
+
173
+ # Carga/atualização de dados remotos
174
+ identity = await usercall.load_identity() # recarrega UserIdentity da API
175
+ identity = await usercall.load_identity(cpf='cpf') # força busca por CPF específico
176
+ userstate = await usercall.load_userstate() # recarrega UserState completo da API
177
+
178
+ # Associação de CPF
179
+ await usercall.associate_cpf(
180
+ cpf='00000000000',
181
+ source='nome_do_menu', # origem da associação (ex: nome do menu)
182
+ phone='', # telefone da empresa (opcional)
183
+ device_id='', # ID do dispositivo (opcional)
184
+ )
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 6. Tipos de Retorno de Rota
190
+
191
+ Cada função de rota deve retornar **um dos tipos** abaixo:
192
+
193
+ ### `RedirectResponse(route)` Redireciona e re-executa imediatamente
194
+ ```python
195
+ return RedirectResponse('choice_start')
196
+ ```
197
+
198
+ ### `Route(current_node)` — Atualiza a rota e aguarda nova mensagem
199
+ ```python
200
+ return Route('aguardando_resposta')
201
+ ```
202
+
203
+ ### `EndChatResponse(end_chat_id, end_chat_name?, observations?)` — Encerra o chat
204
+ ```python
205
+ return EndChatResponse('voll_ended')
206
+ return EndChatResponse('', end_chat_name='Encerrado pelo usuário', observations='motivo')
207
+ ```
208
+
209
+ ### `TransferToMenu(menu, user_message, route?)` — Transfere para outro menu
210
+ ```python
211
+ return TransferToMenu('p0299_suporte_ti', 'Transferindo...')
212
+ return TransferToMenu('p0299_suporte_ti', 'Transferindo...', route='etapa_inicial') # inicia em rota específica
213
+ ```
214
+
215
+ ### `TransferToHuman(campaign_id?, campaign_name?, observations?)` — Transfere para humano
216
+ ```python
217
+ return TransferToHuman(campaign_name='Suporte N2')
218
+ ```
219
+
220
+ ### `BackgroundTask(async_func, *args, **kwargs)` — Executa tarefa em background e encadeia o retorno
221
+ ```python
222
+ async def processar(usercall: UserCall):
223
+ await usercall.send('Processando...')
224
+ return EndChatResponse('concluido')
225
+
226
+ return BackgroundTask(processar, usercall)
227
+ ```
228
+
229
+ ### Lista/tupla — Múltiplas respostas sequenciais
230
+ ```python
231
+ return [
232
+ Message('Primeira mensagem'),
233
+ Message('Segunda mensagem'),
234
+ RedirectResponse('proxima_rota'),
235
+ ]
236
+ ```
237
+
238
+ ---
239
+
240
+ ## 7. Mensagens
241
+
242
+ ```python
243
+ from chatgraph import Message, Button, File, TextMessage, SendType
244
+
245
+ # Texto simples
246
+ await usercall.send('Olá!')
247
+ await usercall.send(Message('Olá!'))
248
+
249
+ # Com botões
250
+ msg = Message(
251
+ 'Escolha uma opção:',
252
+ buttons=[
253
+ Button('Opção 1'), # POSTBACK simples
254
+ Button('Ver mais', detail='payload_123'), # com payload
255
+ Button('Cancelar'),
256
+ ],
257
+ )
258
+ await usercall.send(msg)
259
+
260
+ # Arquivo por path local
261
+ file = File.from_path('caminho/para/imagem.png')
262
+ await usercall.send(file)
263
+
264
+ # Arquivo em mensagem
265
+ msg_com_arquivo = Message(file=file)
266
+ await usercall.send(msg_com_arquivo)
267
+ ```
268
+
269
+ ### `Button` — campos
270
+ ```python
271
+ from chatgraph import Button
272
+ from chatgraph.models.message import ButtonType # não exportado no __init__ top-level
273
+
274
+ Button(
275
+ title='Texto do botão', # exibido para o usuário
276
+ detail='payload', # dados enviados ao pressionar (opcional)
277
+ type=ButtonType.POSTBACK, # ButtonType.POSTBACK (padrão) | ButtonType.URL
278
+ )
279
+ ```
280
+
281
+ ### `TextMessage` dataclass
282
+ ```python
283
+ from chatgraph import TextMessage
284
+
285
+ # Normalmente criado automaticamente por Message(str)
286
+ # Acesso direto ao conteúdo recebido:
287
+ usercall.content_message # equivale a mensagem_recebida.text_message.detail
288
+ ```
289
+
290
+ ### `SendType` — enum para arquivos
291
+ ```python
292
+ from chatgraph import SendType
293
+
294
+ # SendType.IMAGE | SendType.VIDEO | SendType.AUDIO | SendType.FILE | SendType.UNKNOWN
295
+ file = File.from_path('video.mp4')
296
+ file.send_type = SendType.VIDEO
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 8. Logging
302
+
303
+ ### Níveis recomendados
304
+
305
+ | Situação | Método | Destino |
306
+ |----------|--------|---------|
307
+ | Dentro de rota, com `usercall` | `usercall.logger.info/debug/warning/error(...)` | `chatgraph_logs/{user_id}_{company_id}.log` |
308
+ | Fora de rota (startup, módulo) | `_logger = get_system_logger()` | `chatgraph_logs/system.log` |
309
+
310
+ ```python
311
+ from chatgraph.logger import get_system_logger, get_user_logger, set_level
312
+
313
+ _logger = get_system_logger()
314
+ _logger.info('App iniciada')
315
+
316
+ # Logger de usuário fora de uma rota (raramente necessário — prefira usercall.logger dentro de rotas)
317
+ user_log = get_user_logger('user123', 'empresa456')
318
+
319
+ # Alterar nível de log em runtime (afeta todos os loggers existentes)
320
+ set_level('DEBUG') # ou logging.DEBUG
321
+ # Equivalente: UserLoggerManager.set_level('DEBUG')
322
+
323
+ @app.route('start')
324
+ async def start(usercall: UserCall):
325
+ usercall.logger.info('Usuário entrou em start')
326
+ usercall.logger.debug(f'Mensagem recebida: {usercall.content_message}')
327
+ try:
328
+ await usercall.send('Olá!')
329
+ except Exception as e:
330
+ usercall.logger.error(f'Erro ao enviar: {e}')
331
+ ```
332
+
333
+ ### Formato do log
334
+ ```
335
+ 2026-05-06 15:13:15,828 | INFO | Mensagem | nome_funcao | user123_empresa456
336
+ ```
337
+
338
+ ---
339
+
340
+ ## 9. Modularização com `ChatbotRouter`
341
+
342
+ Para projetos maiores, organize rotas em módulos separados:
343
+
344
+ ```python
345
+ # rotas/suporte.py
346
+ from chatgraph import ChatbotRouter, UserCall, Route, RedirectResponse
347
+
348
+ router = ChatbotRouter()
349
+
350
+ @router.route('suporte')
351
+ async def suporte(usercall: UserCall):
352
+ await usercall.send('Como posso ajudar?')
353
+ return Route('aguardar_resposta_suporte')
354
+
355
+ @router.route('aguardar_resposta_suporte')
356
+ async def aguardar(usercall: UserCall):
357
+ await usercall.send(f'Você disse: {usercall.content_message}')
358
+ return RedirectResponse('start')
359
+
360
+ # auth_level também pode ser definido em rotas de router
361
+ @router.route('area_rh', auth_level='internal')
362
+ async def area_rh(usercall: UserCall):
363
+ await usercall.send(f'Olá, {usercall.user.internal.cargo}!')
364
+ return RedirectResponse('start')
365
+ ```
366
+
367
+ ```python
368
+ # main.py
369
+ from chatgraph import ChatbotApp
370
+ from rotas.suporte import router as suporte_router
371
+
372
+ app = ChatbotApp()
373
+ app.include_router(suporte_router)
374
+
375
+ app.start()
376
+ ```
377
+
378
+ > Não há prefixo automático — o nome de cada `@router.route('nome')` é o nome final da rota.
379
+
380
+ `ChatbotRouter` também pode absorver outro `ChatbotRouter` com `include_router()`:
381
+
382
+ ```python
383
+ # rotas/geral.py
384
+ from chatgraph import ChatbotRouter
385
+ from rotas.suporte import router as suporte_router
386
+ from rotas.vendas import router as vendas_router
387
+
388
+ router = ChatbotRouter()
389
+ router.include_router(suporte_router)
390
+ router.include_router(vendas_router)
391
+ ```
392
+
393
+ ---
394
+
395
+ ## 10. Funções Padrão (`default_functions`)
396
+
397
+ O `ChatbotApp` intercepta mensagens **antes** de despachar para a rota quando o texto corresponder a um padrão regex registrado em `default_functions`. Após a execução da função padrão, `content_message` é zerado.
398
+
399
+ ### Comportamento embutido — `voltar`
400
+
401
+ Por padrão, qualquer mensagem que corresponda a `^\s*(voltar)\s*$` (case-insensitive) é interceptada e executa `voltar()`, que redireciona para o nó anterior via `route.get_previous()`.
402
+
403
+ ```python
404
+ # Comportamento automático — nenhuma rota necessária
405
+ # Usuário digita "voltar" → retorna para a rota anterior
406
+ ```
407
+
408
+ ### Customizar ou desabilitar as funções padrão
409
+
410
+ ```python
411
+ from chatgraph import ChatbotApp
412
+
413
+ # Desabilitar o voltar
414
+ app = ChatbotApp(default_functions={})
415
+
416
+ # Adicionar função customizada
417
+ from chatgraph import UserCall, Route, RedirectResponse
418
+
419
+ async def ajuda(route: Route, usercall: UserCall):
420
+ await usercall.send('Comandos disponíveis: voltar, sair')
421
+ return RedirectResponse(route.current_node)
422
+
423
+ app = ChatbotApp(default_functions={
424
+ r'^\s*(voltar)\s*$': voltar, # manter o padrão
425
+ r'^\s*(ajuda|help)\s*$': ajuda, # adicionar novo
426
+ })
427
+ ```
428
+
429
+ > As funções padrão recebem `(route: Route, usercall: UserCall)` e têm acesso às mesmas respostas de rota. `auth_level` não é verificado para funções padrão.
430
+
431
+ ---
432
+
433
+ ## 11. Fluxo de Navegação
434
+
435
+ - Rota inicial obrigatória: `start`
436
+ - Sub-rotas usam notação de ponto internamente: `start.choice.confirm`
437
+ - `Route(node)` adiciona o nó ao caminho atual e aguarda nova mensagem
438
+ - `RedirectResponse(route)` troca para o nó e re-executa imediatamente (sem aguardar)
439
+ - `rota.current_node` → último segmento (ex: `"confirm"`)
440
+ - `rota.previous` rota anterior no histórico
441
+ - `rota.get_next('sub_rota')` → constrói o próximo `Route` e valida se existe na lista de rotas disponíveis
442
+
443
+ ---
444
+
445
+ ## 12. Controle de Acesso — `auth_level` e `guard`
446
+
447
+ Cada rota pode declarar um `auth_level` que é validado pelo guard antes de executar a função.
448
+
449
+ ```python
450
+ # Níveis suportados pelo default_guard:
451
+ # 'read' → usercall.user.identity.auth_level >= AuthLevel.READ
452
+ # 'write' → usercall.user.identity.auth_level >= AuthLevel.WRITE
453
+ # 'internal' usercall.user.internal não pode ser None
454
+
455
+ @app.route('area_restrita', auth_level='internal')
456
+ async def area_restrita(usercall: UserCall):
457
+ await usercall.send('Acesso autorizado!')
458
+ return RedirectResponse('menu_principal')
459
+
460
+ @app.route('dados_sensiveis', auth_level='write')
461
+ async def dados_sensiveis(usercall: UserCall):
462
+ await usercall.send('Você tem permissão de escrita.')
463
+ return EndChatResponse('concluido')
464
+ ```
465
+
466
+ Quando o acesso é negado, o `default_guard` redireciona para `menu_id_positiva` e salva `pending_route`, `pending_menu` e `pending_auth_level` na observação da sessão.
467
+
468
+ ### Guard customizado
469
+
470
+ ```python
471
+ from chatgraph import ChatbotApp, UserCall, default_guard
472
+ from chatgraph.types.end_types import TransferToMenu
473
+
474
+ async def meu_guard(usercall: UserCall, auth_level: str) -> TransferToMenu | None:
475
+ if auth_level == 'admin' and usercall.user.data.email != 'admin@empresa.com':
476
+ return TransferToMenu('menu_acesso_negado', '')
477
+ return None # None = acesso liberado
478
+
479
+ app = ChatbotApp(guard=meu_guard)
480
+ ```
481
+
482
+ > Se o guard retornar `None`, a rota é executada normalmente. Qualquer outro tipo de retorno é processado como resposta de rota (ex: `TransferToMenu`, `RedirectResponse`).
483
+
484
+ ---
485
+
486
+ ## 13. Exemplo Completo
487
+
488
+ ```python
489
+ from chatgraph import (
490
+ ChatbotApp, UserCall, Route,
491
+ Message, Button, File,
492
+ EndChatResponse, RedirectResponse, TransferToMenu,
493
+ )
494
+ from chatgraph.logger import get_system_logger
495
+ from dotenv import load_dotenv
496
+
497
+ load_dotenv()
498
+ _logger = get_system_logger()
499
+ _logger.info('Aplicação iniciada')
500
+ app = ChatbotApp()
501
+
502
+
503
+ @app.route('start')
504
+ async def start(usercall: UserCall, rota: Route):
505
+ usercall.logger.info('Usuário entrou em start')
506
+ await usercall.send(
507
+ Message('Bem-vindo! Escolha:', buttons=[Button('Suporte'), Button('Sair')])
508
+ )
509
+ return Route('aguardar_escolha')
510
+
511
+
512
+ @app.route('aguardar_escolha')
513
+ async def aguardar_escolha(usercall: UserCall):
514
+ resposta = usercall.content_message
515
+ usercall.logger.info(f'Escolha recebida: {resposta}')
516
+
517
+ if resposta == 'Suporte':
518
+ return TransferToMenu('menu_suporte', 'Transferindo para suporte...')
519
+ elif resposta == 'Sair':
520
+ return EndChatResponse('encerrado')
521
+ else:
522
+ usercall.logger.warning(f'Opção inválida: {resposta}')
523
+ await usercall.send('Opção inválida. Tente novamente.')
524
+ return RedirectResponse('start')
525
+
526
+
527
+ app.start()
528
+ ```