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.
@@ -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
+ }