chatgraph 0.6.4__py3-none-any.whl
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/__init__.py +35 -0
- chatgraph/auth/credentials.py +71 -0
- chatgraph/bot/chatbot_model.py +238 -0
- chatgraph/bot/chatbot_router.py +85 -0
- chatgraph/bot/default_functions.py +24 -0
- chatgraph/cli/__init__.py +187 -0
- chatgraph/error/chatbot_error.py +57 -0
- chatgraph/error/route_error.py +26 -0
- chatgraph/gRPC/gRPCCall.py +189 -0
- chatgraph/messages/message_consumer.py +212 -0
- chatgraph/models/actions.py +138 -0
- chatgraph/models/message.py +350 -0
- chatgraph/models/userstate.py +214 -0
- chatgraph/pb/router.proto +151 -0
- chatgraph/pb/router_pb2.py +79 -0
- chatgraph/pb/router_pb2_grpc.py +902 -0
- chatgraph/services/__init__.py +0 -0
- chatgraph/services/router_http_client.py +451 -0
- chatgraph/types/background_task.py +27 -0
- chatgraph/types/end_types.py +75 -0
- chatgraph/types/route.py +104 -0
- chatgraph/types/usercall.py +229 -0
- chatgraph-0.6.4.dist-info/METADATA +521 -0
- chatgraph-0.6.4.dist-info/RECORD +27 -0
- chatgraph-0.6.4.dist-info/WHEEL +4 -0
- chatgraph-0.6.4.dist-info/entry_points.txt +3 -0
- chatgraph-0.6.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modelos de dados para mensagens do chatbot.
|
|
3
|
+
|
|
4
|
+
Este módulo contém as dataclasses e enums para representar mensagens,
|
|
5
|
+
botões, arquivos e seus tipos no sistema de chatbot.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import List, Optional, Union
|
|
12
|
+
import httpx
|
|
13
|
+
import os
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
MessageTypes = Union[str, float, int]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class File:
|
|
21
|
+
"""
|
|
22
|
+
Representa um arquivo no sistema.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
id: ID único do arquivo
|
|
26
|
+
url: URL do arquivo
|
|
27
|
+
name: Nome do arquivo (ex: "document.pdf")
|
|
28
|
+
mime_type: Tipo MIME do arquivo
|
|
29
|
+
size: Tamanho em bytes
|
|
30
|
+
object_key: Chave do objeto no storage
|
|
31
|
+
created_at: Data/hora de criação (ISO 8601)
|
|
32
|
+
expires_after_days: Dias até expiração
|
|
33
|
+
actualized_at: Data/hora de última atualização (ISO 8601)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
id: str = ''
|
|
37
|
+
url: str = ''
|
|
38
|
+
name: str = ''
|
|
39
|
+
mime_type: str = ''
|
|
40
|
+
size: int = 0
|
|
41
|
+
object_key: str = ''
|
|
42
|
+
created_at: str = ''
|
|
43
|
+
expires_after_days: int = 0
|
|
44
|
+
actualized_at: str = ''
|
|
45
|
+
bytes_data: Optional[bytes] = None
|
|
46
|
+
hash_id: Optional[str] = ''
|
|
47
|
+
|
|
48
|
+
def is_empty(self) -> bool:
|
|
49
|
+
"""Verifica se o arquivo está vazio."""
|
|
50
|
+
return not self.id and not self.url and not self.name
|
|
51
|
+
|
|
52
|
+
def extension(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Retorna a extensão do arquivo em minúsculas.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Extensão do arquivo (ex: ".pdf")
|
|
58
|
+
"""
|
|
59
|
+
if not self.name or '.' not in self.name:
|
|
60
|
+
return ''
|
|
61
|
+
return self.name[self.name.rfind('.') :].lower()
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict:
|
|
64
|
+
"""Converte para dicionário."""
|
|
65
|
+
data = {
|
|
66
|
+
'id': self.id,
|
|
67
|
+
'url': self.url,
|
|
68
|
+
'name': self.name,
|
|
69
|
+
'mime_type': self.mime_type,
|
|
70
|
+
'size': self.size,
|
|
71
|
+
'object_key': self.object_key,
|
|
72
|
+
'created_at': self.created_at,
|
|
73
|
+
'expires_after_days': self.expires_after_days,
|
|
74
|
+
'actualized_at': self.actualized_at,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: dict) -> 'File':
|
|
81
|
+
"""Cria instância a partir de dicionário."""
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
id=data.get('id', ''),
|
|
85
|
+
url=data.get('url', ''),
|
|
86
|
+
name=data.get('name', ''),
|
|
87
|
+
mime_type=data.get('mime_type', ''),
|
|
88
|
+
size=data.get('size', 0),
|
|
89
|
+
object_key=data.get('object_key', ''),
|
|
90
|
+
created_at=data.get('created_at', ''),
|
|
91
|
+
expires_after_days=data.get('expires_after_days', 0),
|
|
92
|
+
actualized_at=data.get('actualized_at', ''),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_path(cls, path: str) -> 'File':
|
|
97
|
+
"""Cria instância a partir de um caminho de arquivo."""
|
|
98
|
+
return cls(
|
|
99
|
+
name=path,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def __check_file_exists(self) -> bool:
|
|
103
|
+
if os.path.isfile(self.name):
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
async def __deal_with_url(self) -> bytes:
|
|
108
|
+
async with httpx.AsyncClient() as client:
|
|
109
|
+
response = await client.get(self.url)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
return response.content
|
|
112
|
+
|
|
113
|
+
async def __deal_with_path(self) -> bytes:
|
|
114
|
+
"""Lê os bytes de um arquivo dado seu caminho."""
|
|
115
|
+
if not await self.__check_file_exists():
|
|
116
|
+
raise ValueError('Arquivo não encontrado para envio.')
|
|
117
|
+
|
|
118
|
+
with open(self.name, 'rb') as file:
|
|
119
|
+
self.bytes_data = file.read()
|
|
120
|
+
return self.bytes_data
|
|
121
|
+
|
|
122
|
+
async def __make_file_hash(
|
|
123
|
+
self,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Gera hash SHA-256 de bytes do arquivo."""
|
|
126
|
+
if not self.bytes_data:
|
|
127
|
+
raise ValueError('Dados do arquivo não carregados para hash.')
|
|
128
|
+
|
|
129
|
+
self.hash_id = hashlib.sha256(self.bytes_data).hexdigest()
|
|
130
|
+
return self.hash_id
|
|
131
|
+
|
|
132
|
+
async def load_file(self):
|
|
133
|
+
if not self.name and not self.url:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
'Nenhum dado de arquivo fornecido para carregamento.'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
if self.url:
|
|
140
|
+
self.bytes_data = await self.__deal_with_url()
|
|
141
|
+
else:
|
|
142
|
+
self.bytes_data = await self.__deal_with_path()
|
|
143
|
+
if not self.hash_id and self.bytes_data:
|
|
144
|
+
self.hash_id = await self.__make_file_hash()
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise ValueError(f'Erro ao carregar arquivo: {e}')
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ButtonType(Enum):
|
|
151
|
+
"""
|
|
152
|
+
Tipo de botão de mensagem.
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
POSTBACK: Botão que envia dados de volta ao sistema
|
|
156
|
+
URL: Botão que abre um link externo
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
POSTBACK = 'postback'
|
|
160
|
+
URL = 'url'
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_string(cls, value: str) -> 'ButtonType':
|
|
164
|
+
"""
|
|
165
|
+
Cria ButtonType a partir de string.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
value: String representando o tipo ("postback" ou "url")
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
ButtonType correspondente
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: Se o valor não for válido
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
return cls(value.lower())
|
|
178
|
+
except ValueError:
|
|
179
|
+
raise ValueError(f'invalid button type: {value}')
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class TextMessage:
|
|
184
|
+
"""
|
|
185
|
+
Mensagem de texto.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
id: ID único da mensagem
|
|
189
|
+
title: Título da mensagem
|
|
190
|
+
detail: Conteúdo detalhado da mensagem
|
|
191
|
+
caption: Legenda da mensagem
|
|
192
|
+
mentioned_ids: IDs de usuários mencionados
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
id: str = ''
|
|
196
|
+
title: str = ''
|
|
197
|
+
detail: str = ''
|
|
198
|
+
caption: str = ''
|
|
199
|
+
mentioned_ids: List[str] = field(default_factory=list)
|
|
200
|
+
|
|
201
|
+
def to_dict(self) -> dict:
|
|
202
|
+
"""Converte para dicionário."""
|
|
203
|
+
return {
|
|
204
|
+
'id': self.id,
|
|
205
|
+
'title': self.title,
|
|
206
|
+
'detail': self.detail,
|
|
207
|
+
'caption': self.caption,
|
|
208
|
+
'mentioned_ids': self.mentioned_ids,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_dict(cls, data: dict) -> 'TextMessage':
|
|
213
|
+
"""Cria instância a partir de dicionário."""
|
|
214
|
+
return cls(
|
|
215
|
+
id=data.get('id', ''),
|
|
216
|
+
title=data.get('title', ''),
|
|
217
|
+
detail=data.get('detail', ''),
|
|
218
|
+
caption=data.get('caption', ''),
|
|
219
|
+
mentioned_ids=data.get('mentioned_ids', []),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass
|
|
224
|
+
class Button:
|
|
225
|
+
"""
|
|
226
|
+
Botão de mensagem interativa.
|
|
227
|
+
|
|
228
|
+
Attributes:
|
|
229
|
+
type: Tipo do botão (POSTBACK ou URL)
|
|
230
|
+
title: Título do botão
|
|
231
|
+
detail: Detalhe/payload do botão
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
title: str = ''
|
|
235
|
+
detail: str = ''
|
|
236
|
+
type: ButtonType = ButtonType.POSTBACK
|
|
237
|
+
|
|
238
|
+
def to_dict(self) -> dict:
|
|
239
|
+
"""Converte para dicionário."""
|
|
240
|
+
return {
|
|
241
|
+
'type': self.type.value,
|
|
242
|
+
'title': self.title,
|
|
243
|
+
'detail': self.detail,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_dict(cls, data: dict) -> 'Button':
|
|
248
|
+
"""Cria instância a partir de dicionário."""
|
|
249
|
+
button_type = ButtonType.from_string(data.get('type', 'postback'))
|
|
250
|
+
return cls(
|
|
251
|
+
type=button_type,
|
|
252
|
+
title=data.get('title', ''),
|
|
253
|
+
detail=data.get('detail', ''),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class Message:
|
|
259
|
+
"""
|
|
260
|
+
Mensagem completa com texto, botões e anexos.
|
|
261
|
+
|
|
262
|
+
Attributes:
|
|
263
|
+
text_message: Conteúdo textual da mensagem
|
|
264
|
+
buttons: Lista de botões interativos
|
|
265
|
+
display_button: Botão de exibição principal
|
|
266
|
+
date_time: Data e hora da mensagem
|
|
267
|
+
file: Arquivo anexado (opcional)
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
text_message: TextMessage = field(default_factory=TextMessage)
|
|
271
|
+
buttons: List[Button] = field(default_factory=list)
|
|
272
|
+
display_button: Optional[Button] = None
|
|
273
|
+
date_time: datetime = field(default_factory=datetime.now)
|
|
274
|
+
file: Optional[File] = field(default_factory=File)
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
text_message: TextMessage | str = '',
|
|
279
|
+
buttons: List[Button] = [],
|
|
280
|
+
display_button: Optional[Button] = None,
|
|
281
|
+
file: Optional[File | str] = None,
|
|
282
|
+
date_time: Optional[datetime] = None,
|
|
283
|
+
):
|
|
284
|
+
self.buttons = buttons
|
|
285
|
+
self.display_button = display_button
|
|
286
|
+
self.date_time = date_time if date_time is not None else datetime.now()
|
|
287
|
+
|
|
288
|
+
self.__load_text_message(text_message)
|
|
289
|
+
self.__load_file(file)
|
|
290
|
+
|
|
291
|
+
def has_buttons(self) -> bool:
|
|
292
|
+
"""Verifica se a mensagem possui botões."""
|
|
293
|
+
return len(self.buttons) > 0
|
|
294
|
+
|
|
295
|
+
def has_file(self) -> bool:
|
|
296
|
+
"""Verifica se a mensagem possui arquivo anexado."""
|
|
297
|
+
return self.file is not None and not self.file.is_empty()
|
|
298
|
+
|
|
299
|
+
def __load_file(self, file: Optional[File | str]) -> None:
|
|
300
|
+
if isinstance(file, str):
|
|
301
|
+
self.file = File(name=file)
|
|
302
|
+
else:
|
|
303
|
+
self.file = file
|
|
304
|
+
|
|
305
|
+
def __load_text_message(self, text_message: TextMessage | str) -> None:
|
|
306
|
+
if isinstance(text_message, str):
|
|
307
|
+
self.text_message = TextMessage(detail=text_message)
|
|
308
|
+
else:
|
|
309
|
+
self.text_message = text_message
|
|
310
|
+
|
|
311
|
+
def to_dict(self) -> dict:
|
|
312
|
+
"""Converte para dicionário."""
|
|
313
|
+
data = {
|
|
314
|
+
'text_message': self.text_message.to_dict(),
|
|
315
|
+
'buttons': [btn.to_dict() for btn in self.buttons],
|
|
316
|
+
'date_time': self.date_time.isoformat(),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if self.display_button:
|
|
320
|
+
data['display_button'] = self.display_button.to_dict()
|
|
321
|
+
if self.file:
|
|
322
|
+
data['file'] = self.file.to_dict()
|
|
323
|
+
|
|
324
|
+
return data
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def from_dict(cls, data: dict) -> 'Message':
|
|
328
|
+
"""Cria instância a partir de dicionário."""
|
|
329
|
+
text_message_data = data.get('text_message', {})
|
|
330
|
+
buttons_data = data.get('buttons', [])
|
|
331
|
+
display_button_data = data.get('display_button')
|
|
332
|
+
|
|
333
|
+
date_time = datetime.now()
|
|
334
|
+
if 'date_time' in data:
|
|
335
|
+
try:
|
|
336
|
+
date_time = datetime.fromisoformat(data['date_time'])
|
|
337
|
+
except (ValueError, TypeError):
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
return cls(
|
|
341
|
+
text_message=TextMessage.from_dict(text_message_data),
|
|
342
|
+
buttons=[Button.from_dict(btn) for btn in buttons_data],
|
|
343
|
+
display_button=(
|
|
344
|
+
Button.from_dict(display_button_data)
|
|
345
|
+
if display_button_data
|
|
346
|
+
else None
|
|
347
|
+
),
|
|
348
|
+
date_time=date_time,
|
|
349
|
+
file=data.get('file'),
|
|
350
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modelos de dados para gerenciamento de estado de usuário.
|
|
3
|
+
|
|
4
|
+
Este módulo contém as dataclasses que representam o estado do usuário
|
|
5
|
+
no sistema de chatbot, incluindo identificação, informações pessoais,
|
|
6
|
+
menu atual e metadados da sessão.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ChatID:
|
|
16
|
+
"""
|
|
17
|
+
Identificador único do chat.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
user_id: ID único do usuário
|
|
21
|
+
company_id: ID da empresa/organização
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
user_id: str
|
|
25
|
+
company_id: str
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
"""Converte para dicionário."""
|
|
29
|
+
return {
|
|
30
|
+
'user_id': self.user_id,
|
|
31
|
+
'company_id': self.company_id,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: dict) -> 'ChatID':
|
|
36
|
+
"""Cria instância a partir de dicionário."""
|
|
37
|
+
return cls(
|
|
38
|
+
user_id=data.get('user_id', ''),
|
|
39
|
+
company_id=data.get('company_id', ''),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class User:
|
|
45
|
+
"""
|
|
46
|
+
Informações do usuário.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
cpf: CPF do usuário (opcional)
|
|
50
|
+
name: Nome do usuário (opcional)
|
|
51
|
+
phone: Telefone do usuário (opcional)
|
|
52
|
+
email: Email do usuário (opcional)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
cpf: Optional[str] = None
|
|
56
|
+
name: Optional[str] = None
|
|
57
|
+
phone: Optional[str] = None
|
|
58
|
+
email: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
"""Converte para dicionário, omitindo campos None."""
|
|
62
|
+
data = {}
|
|
63
|
+
if self.cpf is not None:
|
|
64
|
+
data['cpf'] = self.cpf
|
|
65
|
+
if self.name is not None:
|
|
66
|
+
data['name'] = self.name
|
|
67
|
+
if self.phone is not None:
|
|
68
|
+
data['phone'] = self.phone
|
|
69
|
+
if self.email is not None:
|
|
70
|
+
data['email'] = self.email
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: dict) -> 'User':
|
|
75
|
+
"""Cria instância a partir de dicionário."""
|
|
76
|
+
return cls(
|
|
77
|
+
cpf=data.get('cpf'),
|
|
78
|
+
name=data.get('name'),
|
|
79
|
+
phone=data.get('phone'),
|
|
80
|
+
email=data.get('email'),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Menu:
|
|
86
|
+
"""
|
|
87
|
+
Informações do menu/departamento.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
id: ID do menu (opcional)
|
|
91
|
+
department_id: ID do departamento (opcional)
|
|
92
|
+
name: Nome do menu (opcional)
|
|
93
|
+
description: Descrição do menu (opcional)
|
|
94
|
+
active: Status de ativação do menu (opcional)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
id: Optional[int] = None
|
|
98
|
+
department_id: Optional[int] = None
|
|
99
|
+
name: Optional[str] = None
|
|
100
|
+
description: Optional[str] = None
|
|
101
|
+
active: Optional[bool] = None
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
"""Converte para dicionário, omitindo campos None."""
|
|
105
|
+
data = {}
|
|
106
|
+
if self.id is not None:
|
|
107
|
+
data['id'] = self.id
|
|
108
|
+
if self.department_id is not None:
|
|
109
|
+
data['department_id'] = self.department_id
|
|
110
|
+
if self.name is not None:
|
|
111
|
+
data['name'] = self.name
|
|
112
|
+
if self.description is not None:
|
|
113
|
+
data['description'] = self.description
|
|
114
|
+
if self.active is not None:
|
|
115
|
+
data['active'] = self.active
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: dict) -> 'Menu':
|
|
120
|
+
"""Cria instância a partir de dicionário."""
|
|
121
|
+
return cls(
|
|
122
|
+
id=data.get('id'),
|
|
123
|
+
department_id=data.get('department_id'),
|
|
124
|
+
name=data.get('name'),
|
|
125
|
+
description=data.get('description'),
|
|
126
|
+
active=data.get('active'),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class UserState:
|
|
132
|
+
"""
|
|
133
|
+
Estado completo do usuário no sistema.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
chat_id: Identificador do chat (obrigatório)
|
|
137
|
+
platform: Plataforma de comunicação (obrigatório)
|
|
138
|
+
session_id: ID da sessão (opcional)
|
|
139
|
+
menu: Menu atual (opcional)
|
|
140
|
+
user: Informações do usuário (opcional)
|
|
141
|
+
route: Rota atual no fluxo (opcional)
|
|
142
|
+
direction_in: Indica se é mensagem de entrada (opcional)
|
|
143
|
+
observation: Observações/contexto adicional (opcional)
|
|
144
|
+
last_update: Data/hora da última atualização (opcional)
|
|
145
|
+
dt_created: Data/hora de criação (opcional)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
chat_id: ChatID
|
|
149
|
+
platform: str
|
|
150
|
+
session_id: Optional[int] = None
|
|
151
|
+
menu: Optional[Menu] = field(default_factory=Menu)
|
|
152
|
+
user: Optional[User] = field(default_factory=User)
|
|
153
|
+
route: str = 'start'
|
|
154
|
+
direction_in: Optional[bool] = None
|
|
155
|
+
observation: Optional[str] = None
|
|
156
|
+
last_update: Optional[str] = None
|
|
157
|
+
dt_created: Optional[str] = None
|
|
158
|
+
|
|
159
|
+
def to_dict(self) -> dict:
|
|
160
|
+
"""Converte para dicionário."""
|
|
161
|
+
data = {
|
|
162
|
+
'chat_id': self.chat_id.to_dict(),
|
|
163
|
+
'platform': self.platform,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if self.session_id is not None:
|
|
167
|
+
data['session_id'] = self.session_id
|
|
168
|
+
if self.menu is not None:
|
|
169
|
+
data['menu'] = self.menu.to_dict()
|
|
170
|
+
if self.user is not None:
|
|
171
|
+
data['user'] = self.user.to_dict()
|
|
172
|
+
if self.route is not None:
|
|
173
|
+
data['route'] = self.route
|
|
174
|
+
if self.direction_in is not None:
|
|
175
|
+
data['direction_in'] = self.direction_in
|
|
176
|
+
if self.observation is not None:
|
|
177
|
+
data['observation'] = self.observation
|
|
178
|
+
if self.last_update is not None:
|
|
179
|
+
data['last_update'] = self.last_update
|
|
180
|
+
if self.dt_created is not None:
|
|
181
|
+
data['dt_created'] = self.dt_created
|
|
182
|
+
|
|
183
|
+
return data
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, data: dict) -> 'UserState':
|
|
187
|
+
"""Cria instância a partir de dicionário."""
|
|
188
|
+
chat_id_data = data.get('chat_id', {})
|
|
189
|
+
menu_data = data.get('menu', {})
|
|
190
|
+
user_data = data.get('user', {})
|
|
191
|
+
|
|
192
|
+
return cls(
|
|
193
|
+
session_id=data.get('session_id'),
|
|
194
|
+
chat_id=ChatID.from_dict(chat_id_data),
|
|
195
|
+
menu=Menu.from_dict(menu_data) if menu_data else Menu(),
|
|
196
|
+
user=User.from_dict(user_data) if user_data else User(),
|
|
197
|
+
route=data.get('route', 'start'),
|
|
198
|
+
direction_in=data.get('direction_in'),
|
|
199
|
+
observation=data.get('observation'),
|
|
200
|
+
platform=data.get('platform', ''),
|
|
201
|
+
last_update=data.get('last_update'),
|
|
202
|
+
dt_created=data.get('dt_created'),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def observation_dict(self) -> dict:
|
|
207
|
+
"""Retorna a observação como dicionário."""
|
|
208
|
+
|
|
209
|
+
if self.observation:
|
|
210
|
+
try:
|
|
211
|
+
return json.loads(self.observation)
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
return {}
|
|
214
|
+
return {}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package chatbot;
|
|
4
|
+
|
|
5
|
+
option go_package = "./chatbot";
|
|
6
|
+
|
|
7
|
+
///// Serviços de Estado do Usuário /////
|
|
8
|
+
service UserStateService {
|
|
9
|
+
rpc InsertUpdateUserState(UserState) returns (RequestStatus);
|
|
10
|
+
rpc SetRoute(RouteRequest) returns (RequestStatus);
|
|
11
|
+
rpc DeleteUserState(ChatID) returns (RequestStatus);
|
|
12
|
+
rpc GetUserState(ChatID) returns (UserState);
|
|
13
|
+
rpc GetAllUserStates(Void) returns (UserStateList);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
///// Serviços de Mensagens /////
|
|
17
|
+
service SendMessage {
|
|
18
|
+
rpc SendMessage(Message) returns (RequestStatus);
|
|
19
|
+
rpc SendImage(FileMessage) returns (RequestStatus);
|
|
20
|
+
rpc SendFile(FileMessage) returns (RequestStatus);
|
|
21
|
+
rpc UploadFile(UploadFileRequest) returns (RequestStatus);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
///// Serviços de Transfer /////
|
|
25
|
+
service Transfer {
|
|
26
|
+
rpc GetAllCampaigns(Void) returns (CampaignsList);
|
|
27
|
+
rpc GetCampaignID(CampaignName) returns (CampaignDetails);
|
|
28
|
+
rpc TransferToHuman(TransferToHumanRequest) returns (RequestStatus);
|
|
29
|
+
rpc TransferToMenu(TransferToMenuRequest) returns (RequestStatus);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
///// Serviços de EndChat /////
|
|
33
|
+
service EndChat {
|
|
34
|
+
rpc GetAllTabulations(Void) returns (TabulationsList);
|
|
35
|
+
rpc GetTabulationID(TabulationName) returns (TabulationDetails);
|
|
36
|
+
rpc EndChat(EndChatRequest) returns (RequestStatus);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
///// Mensagens Compartilhadas /////
|
|
40
|
+
message Void {}
|
|
41
|
+
|
|
42
|
+
message RequestStatus {
|
|
43
|
+
bool status = 1;
|
|
44
|
+
string message = 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
message ChatID {
|
|
48
|
+
string user_id = 1;
|
|
49
|
+
string company_id = 2;
|
|
50
|
+
}
|
|
51
|
+
///// Mensagens para Estado do Usuário /////
|
|
52
|
+
message UserState {
|
|
53
|
+
ChatID chat_id = 1;
|
|
54
|
+
string menu = 2;
|
|
55
|
+
string route = 3;
|
|
56
|
+
string protocol = 4;
|
|
57
|
+
string observation = 5;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
message UserStateList {
|
|
61
|
+
repeated UserState user_states = 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
message RouteRequest {
|
|
65
|
+
ChatID chat_id = 1;
|
|
66
|
+
string route = 2;
|
|
67
|
+
}
|
|
68
|
+
///// Mensagens para Serviços de Mensagens /////
|
|
69
|
+
message TextMessage {
|
|
70
|
+
string type = 1;
|
|
71
|
+
string url = 2;
|
|
72
|
+
string filename = 3;
|
|
73
|
+
string title = 4;
|
|
74
|
+
string detail = 5;
|
|
75
|
+
string caption = 6;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
message Button{
|
|
79
|
+
string type = 1;
|
|
80
|
+
string title = 2;
|
|
81
|
+
string detail = 3;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
message Message {
|
|
85
|
+
ChatID chat_id = 1;
|
|
86
|
+
TextMessage message = 2;
|
|
87
|
+
repeated Button buttons = 3;
|
|
88
|
+
Button display_button = 4;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
message FileMessage {
|
|
92
|
+
Message message = 1;
|
|
93
|
+
string file_id = 2;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
message UploadFileRequest {
|
|
97
|
+
string file_url = 1;
|
|
98
|
+
string file_type = 2;
|
|
99
|
+
string file_extension = 3;
|
|
100
|
+
string expiration = 4;
|
|
101
|
+
bytes file_content = 5;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
///// Mensagens para Serviços de Transfer /////
|
|
105
|
+
message TransferToHumanRequest{
|
|
106
|
+
ChatID chat_id = 1;
|
|
107
|
+
string campaign_id = 2;
|
|
108
|
+
string observation = 3;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
message TransferToMenuRequest{
|
|
112
|
+
ChatID chat_id = 1;
|
|
113
|
+
string menu = 2;
|
|
114
|
+
string route = 3;
|
|
115
|
+
string user_message = 4;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
message TabulationName{
|
|
119
|
+
string name = 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
message TabulationDetails{
|
|
123
|
+
string id = 1;
|
|
124
|
+
string name = 2;
|
|
125
|
+
string last_update = 3;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
message TabulationsList{
|
|
129
|
+
repeated TabulationDetails tabulations = 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
///// Serviços de EndChat /////
|
|
133
|
+
message EndChatRequest {
|
|
134
|
+
ChatID chat_id = 1;
|
|
135
|
+
string tabulation_id = 2;
|
|
136
|
+
string observation = 3;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
message CampaignName {
|
|
140
|
+
string name = 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
message CampaignDetails {
|
|
144
|
+
string id = 1;
|
|
145
|
+
string name = 2;
|
|
146
|
+
string last_update = 3;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
message CampaignsList {
|
|
150
|
+
repeated CampaignDetails campaigns = 1;
|
|
151
|
+
}
|