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.
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/skills/chatgraph-framework/SKILL.md +528 -506
- {chatgraph-1.2.1 → chatgraph-1.2.3}/PKG-INFO +1 -1
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/__init__.py +2 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/default_guard.py +10 -1
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/__init__.py +1 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/user_logger.py +18 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/messages/message_consumer.py +70 -48
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/services/router_http_client.py +106 -28
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/usercall.py +126 -23
- chatgraph-1.2.3/example.py +98 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/pyproject.toml +80 -80
- chatgraph-1.2.3/tests/unit/test_message_consumer.py +364 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_router_http_client.py +209 -10
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_user_logger.py +35 -1
- chatgraph-1.2.3/tests/unit/test_usercall.py +400 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/uv.lock +1139 -1139
- chatgraph-1.2.1/example.py +0 -127
- chatgraph-1.2.1/tests/unit/test_usercall.py +0 -75
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.env.example +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/SkillManager.agent.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/architect.agent.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/code-reviewer.agent.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/agents/developer.agent.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.github/copilot-instructions.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.gitignore +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/.python-version +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/LICENSE +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/README.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/auth/credentials.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/chatbot_model.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/chatbot_router.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/bot/default_functions.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/cli/__init__.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/container/container.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/error/chatbot_error.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/error/route_error.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/gRPC/gRPCCall.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/logger/logger.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/actions.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/http_responses.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/message.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/models/userstate.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router.proto +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router_pb2.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/pb/router_pb2_grpc.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/services/__init__.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/background_task.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/end_types.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/chatgraph/types/route.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/docs/gaps-go-to-python.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/docs/gaps-python-to-go.md +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/example2.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/jsons/voll_return.json +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/poetry.lock +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/__init__.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/__init__.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/conftest.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/integration/test_router_client_integration.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/__init__.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/conftest.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_default_guard.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_guard.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_models_actions.py +0 -0
- {chatgraph-1.2.1 → chatgraph-1.2.3}/tests/unit/test_models_message.py +0 -0
- {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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
###
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
from chatgraph.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
```
|