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.
File without changes
@@ -0,0 +1,451 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ import httpx
4
+
5
+ from ..models.userstate import UserState, ChatID
6
+ from ..models.message import Message, File
7
+ from ..models.actions import EndAction
8
+
9
+
10
+ class RouterHTTPClient:
11
+ """
12
+ Cliente HTTP para serviços de roteamento de mensagens.
13
+
14
+ Utiliza httpx.AsyncClient para comunicação assíncrona com o backend
15
+ do sistema de chatbot, incluindo envio de mensagens, gerenciamento
16
+ de estado e transferências de chat.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ base_url: str,
22
+ username: Optional[str] = None,
23
+ password: Optional[str] = None,
24
+ timeout: float = 30.0,
25
+ ):
26
+ """
27
+ Inicializa o cliente HTTP.
28
+
29
+ Args:
30
+ base_url: URL base da API (ex: "https://api.example.com")
31
+ username: Nome de usuário para autenticação (opcional)
32
+ password: Senha para autenticação (opcional)
33
+ timeout: Timeout para requisições em segundos (padrão: 30.0)
34
+ """
35
+ self.base_url = base_url.rstrip('/')
36
+ self.timeout = timeout
37
+
38
+ # Configurar autenticação básica se fornecida
39
+ auth = None
40
+ if username and password:
41
+ auth = httpx.BasicAuth(username, password)
42
+
43
+ # Criar cliente assíncrono
44
+ self._client = httpx.AsyncClient(
45
+ base_url=self.base_url,
46
+ auth=auth,
47
+ timeout=httpx.Timeout(timeout),
48
+ headers={
49
+ 'Accept': 'application/json',
50
+ # 'Content-Type': 'application/json',
51
+ },
52
+ verify=False,
53
+ trust_env=True,
54
+ follow_redirects=True,
55
+ )
56
+
57
+ async def close(self):
58
+ """Fecha a conexão do cliente HTTP."""
59
+ await self._client.aclose()
60
+
61
+ async def __aenter__(self):
62
+ """Context manager entry."""
63
+ return self
64
+
65
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
66
+ """Context manager exit."""
67
+ await self.close()
68
+
69
+ # Sessions Methods
70
+ async def get_all_sessions(self) -> list[UserState]:
71
+ """
72
+ Obtém todas as sessões ativas.
73
+
74
+ Returns:
75
+ Lista de UserState com todas as sessões ativas.
76
+
77
+ Raises:
78
+ Exception: Se houver erro na comunicação.
79
+ """
80
+ endpoint = '/session/'
81
+
82
+ response = await self._client.get(endpoint)
83
+ response.raise_for_status()
84
+ response_data = response.json()
85
+ status = response_data.get('status')
86
+ message = response_data.get('message')
87
+ sessions_data = response_data.get('data', [])
88
+
89
+ if not status:
90
+ raise Exception(f'Erro ao buscar as Sessões: {message}')
91
+
92
+ sessions = [UserState.from_dict(item) for item in sessions_data]
93
+ return sessions
94
+
95
+ async def get_session_by_chat_id(
96
+ self,
97
+ chat_id: ChatID,
98
+ ) -> Optional[UserState]:
99
+ endpoint = '/session/'
100
+ params = {
101
+ 'user_id': chat_id.user_id,
102
+ 'company_id': chat_id.company_id,
103
+ }
104
+
105
+ response = await self._client.get(endpoint, params=params)
106
+ response.raise_for_status()
107
+ response_data = response.json()
108
+ status = response_data.get('status')
109
+ message = response_data.get('message')
110
+ session_data = response_data.get('data')
111
+
112
+ if not status:
113
+ raise Exception(f'Erro ao buscar a Sessão: {message}')
114
+
115
+ return UserState.from_dict(session_data)
116
+
117
+ async def start_session(self, user_state: UserState) -> Any:
118
+ """
119
+ Inicia uma nova sessão de chat.
120
+
121
+ Args:
122
+ user_state: Estado inicial do usuário.
123
+
124
+ Returns:
125
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
126
+
127
+ Raises:
128
+ Exception: Se houver erro na comunicação.
129
+ """
130
+ endpoint = '/session/start/'
131
+
132
+ response = await self._client.post(
133
+ endpoint,
134
+ json=user_state.to_dict(),
135
+ )
136
+ response.raise_for_status()
137
+ response_data = response.json()
138
+ status = response_data.get('status')
139
+ message = response_data.get('message')
140
+
141
+ if not status:
142
+ raise Exception(f'Erro ao iniciar sessão: {message}')
143
+
144
+ return response_data
145
+
146
+ async def set_session_route(self, chat_id: ChatID, route: str) -> Any:
147
+ """
148
+ Atualiza a rota da sessão de chat.
149
+
150
+ Args:
151
+ chat_id: Identificador do chat.
152
+ route: Nova rota para a sessão.
153
+
154
+ Returns:
155
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
156
+
157
+ Raises:
158
+ Exception: Se houver erro na comunicação.
159
+ """
160
+ endpoint = '/session/route/'
161
+
162
+ payload = {
163
+ 'chat_id': chat_id.to_dict(),
164
+ 'route': route,
165
+ }
166
+
167
+ response = await self._client.post(
168
+ endpoint,
169
+ json=payload,
170
+ )
171
+ response.raise_for_status()
172
+ response_data = response.json()
173
+ status = response_data.get('status')
174
+ message = response_data.get('message')
175
+
176
+ if not status:
177
+ raise Exception(f'Erro ao atualizar rota da sessão: {message}')
178
+
179
+ return response_data
180
+
181
+ async def update_session_observation(
182
+ self,
183
+ chat_id: ChatID,
184
+ observation: str,
185
+ ) -> Any:
186
+ """
187
+ Atualiza a observação da sessão de chat.
188
+
189
+ Args:
190
+ chat_id: Identificador do chat.
191
+ observation: Nova observação para a sessão.
192
+
193
+ Returns:
194
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
195
+
196
+ Raises:
197
+ Exception: Se houver erro na comunicação.
198
+ """
199
+ endpoint = '/session/observation/'
200
+
201
+ payload = {
202
+ 'chat_id': chat_id.to_dict(),
203
+ 'observation': observation,
204
+ }
205
+
206
+ response = await self._client.post(
207
+ endpoint,
208
+ json=payload,
209
+ )
210
+ response.raise_for_status()
211
+ response_data = response.json()
212
+ status = response_data.get('status')
213
+ message = response_data.get('message')
214
+
215
+ if not status:
216
+ raise Exception(
217
+ f'Erro ao atualizar observação da sessão: {message}'
218
+ )
219
+
220
+ return response_data
221
+
222
+ # Messages Methods
223
+ async def send_message(
224
+ self,
225
+ message_data: Message,
226
+ user_state: UserState,
227
+ ) -> Any:
228
+ """
229
+ Envia uma mensagem de texto ao usuário.
230
+
231
+ Args:
232
+ message_data: Dicionário contendo os dados da mensagem:
233
+ - chat_id: ID do chat (user_id, company_id)
234
+ - type: Tipo da mensagem
235
+ - detail: Conteúdo da mensagem
236
+
237
+ Returns:
238
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
239
+
240
+ Raises:
241
+ Exception: Se houver erro na comunicação.
242
+ """
243
+ endpoint = '/messages/send/'
244
+
245
+ payload = {
246
+ 'message': message_data.to_dict(),
247
+ 'user_state': user_state.to_dict(),
248
+ }
249
+ response = await self._client.post(
250
+ endpoint,
251
+ json=payload,
252
+ )
253
+ response.raise_for_status()
254
+ response_data = response.json()
255
+ status = response_data.get('status')
256
+ message = response_data.get('message')
257
+
258
+ if not status:
259
+ raise Exception(f'Erro ao enviar mensagem: {message}')
260
+
261
+ return response_data
262
+
263
+ # Files Methods
264
+ async def get_file(self, file_id: str) -> File:
265
+ """
266
+ Obtém um arquivo (imagem) pelo ID.
267
+
268
+ Args:
269
+ file_id: ID único do arquivo.
270
+
271
+ Returns:
272
+ Objeto de resposta com atributos 'status' e 'file_content'.
273
+
274
+ Raises:
275
+ Exception: Se houver erro na comunicação.
276
+ """
277
+ endpoint = f'/files/{file_id}/'
278
+
279
+ response = await self._client.get(endpoint)
280
+ response.raise_for_status()
281
+ response_data = response.json()
282
+ status = response_data.get('status')
283
+ message = response_data.get('message')
284
+ file_data = response_data.get('data')
285
+
286
+ if not status:
287
+ raise Exception(f'Erro ao buscar arquivo: {message}')
288
+
289
+ return File.from_dict(file_data)
290
+
291
+ async def upload_file(self, file_data: bytes) -> File:
292
+ """
293
+ Faz upload de um arquivo (imagem) para o servidor.
294
+
295
+ Args:
296
+ file_data: Dicionário contendo os dados do arquivo:
297
+ - file_type: Tipo do arquivo ("file" ou "link")
298
+ - file_content: Bytes do arquivo (se type="file")
299
+ - file_extension: Extensão do arquivo (se type="file")
300
+ - file_url: URL do arquivo (se type="link")
301
+ - expiration: Data de expiração (opcional)
302
+
303
+ Returns:
304
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
305
+
306
+ Raises:
307
+ Exception: Se houver erro na comunicação.
308
+ """
309
+ endpoint = '/files/upload/'
310
+
311
+ payload = {
312
+ 'content': file_data,
313
+ }
314
+ response = await self._client.post(
315
+ endpoint,
316
+ files=payload,
317
+ )
318
+ response.raise_for_status()
319
+ response_data = response.json()
320
+ status = response_data.get('status')
321
+ message = response_data.get('message')
322
+ response_data = response_data.get('data')
323
+
324
+ if not status:
325
+ raise Exception(f'Erro ao fazer upload do arquivo: {message}')
326
+
327
+ file = File.from_dict(response_data)
328
+ return file
329
+
330
+ async def delete_file(self, file_id: str) -> Any:
331
+ """
332
+ Deleta um arquivo (imagem) pelo ID.
333
+
334
+ Args:
335
+ file_id: ID único do arquivo.
336
+
337
+ Returns:
338
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
339
+
340
+ Raises:
341
+ Exception: Se houver erro na comunicação.
342
+ """
343
+ endpoint = f'/files/{file_id}'
344
+
345
+ response = await self._client.delete(endpoint)
346
+ response.raise_for_status()
347
+ response_data = response.json()
348
+ status = response_data.get('status')
349
+ message = response_data.get('message')
350
+
351
+ if not status:
352
+ raise Exception(f'Erro ao deletar arquivo: {message}')
353
+
354
+ return response_data
355
+
356
+ # EndAction Methods
357
+ async def end_chat(
358
+ self,
359
+ chat_id: ChatID,
360
+ end_action: EndAction,
361
+ origin: str,
362
+ ) -> Any:
363
+ """
364
+ Encerra o atendimento com tabulação.
365
+
366
+ Args:
367
+ end_data: Dicionário contendo:
368
+ - chat_id: ID do chat (user_id, company_id)
369
+ - end_action: ID da tabulação de encerramento
370
+ - observation: Observação sobre o encerramento
371
+
372
+ Returns:
373
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
374
+
375
+ Raises:
376
+ Exception: Se houver erro na comunicação.
377
+ """
378
+ endpoint = '/session/end/'
379
+ payload = {
380
+ 'chat_id': chat_id.to_dict(),
381
+ 'end_action': end_action.to_dict(),
382
+ 'origin': origin,
383
+ }
384
+
385
+ response = await self._client.post(
386
+ endpoint,
387
+ json=payload,
388
+ )
389
+ response.raise_for_status()
390
+ response_data = response.json()
391
+ status = response_data.get('status')
392
+ message = response_data.get('message')
393
+
394
+ if not status:
395
+ raise Exception(f'Erro ao encerrar chat: {message}')
396
+
397
+ return response_data
398
+
399
+ async def get_end_action(
400
+ self,
401
+ end_action_id: str = '',
402
+ end_action_name: str = '',
403
+ ) -> Any:
404
+ """
405
+ Obtém uma ação de encerramento pelo ID.
406
+
407
+ Args:
408
+ end_action_id: ID único da ação de encerramento.
409
+
410
+ Returns:
411
+ Objeto de resposta com atributos 'status' e 'end_action'.
412
+
413
+ Raises:
414
+ Exception: Se houver erro na comunicação.
415
+ """
416
+ endpoint = '/end_actions/'
417
+ params = {
418
+ 'id': end_action_id,
419
+ 'name': end_action_name,
420
+ }
421
+
422
+ response = await self._client.get(endpoint, params=params)
423
+ response.raise_for_status()
424
+ response_data = response.json()
425
+ status = response_data.get('status')
426
+ message = response_data.get('message')
427
+ end_action_data = response_data.get('data')
428
+
429
+ if not status:
430
+ raise Exception(f'Erro ao buscar ação de encerramento: {message}')
431
+
432
+ return EndAction.from_dict(end_action_data)
433
+
434
+ # ToDo Methods
435
+ async def transfer_to_menu(self, transfer_data: Dict[str, Any]) -> Any:
436
+ """
437
+ Transfere o chat para outro menu do fluxo.
438
+
439
+ Args:
440
+ transfer_data: Dicionário contendo:
441
+ - chat_id: ID do chat (user_id, company_id)
442
+ - menu: Nome do menu de destino
443
+ - user_message: Mensagem do usuário
444
+
445
+ Returns:
446
+ Objeto de resposta com atributo 'status' indicando sucesso/falha.
447
+
448
+ Raises:
449
+ Exception: Se houver erro na comunicação.
450
+ """
451
+ pass
@@ -0,0 +1,27 @@
1
+ import asyncio
2
+ from typing import Any, Callable
3
+ import threading
4
+
5
+ class BackgroundTask:
6
+ def __init__(self, func: Callable, *args: Any, **kwargs: Any) -> None:
7
+ """
8
+ Inicia uma função de forma assíncrona em segundo plano e printa sua saída.
9
+
10
+ Args:
11
+ func (Callable): A função a ser executada.
12
+ args (Any): Argumentos posicionais a serem passados para a função.
13
+ kwargs (Any): Argumentos nomeados a serem passados para a função.
14
+ """
15
+
16
+ if not asyncio.iscoroutinefunction(func):
17
+ raise TypeError("A função fornecida deve ser uma coroutine (async def).")
18
+
19
+ self.args = args
20
+ self.kwargs = kwargs
21
+ self.func = func
22
+
23
+ async def run(self):
24
+ """
25
+ Executa a função em segundo plano.
26
+ """
27
+ return await self.func(*self.args, **self.kwargs)
@@ -0,0 +1,75 @@
1
+ class RedirectResponse:
2
+ """
3
+ Representa uma resposta que redireciona o fluxo do chatbot para uma nova rota.
4
+
5
+ Atributos:
6
+ route (str): A rota para a qual o chatbot deve redirecionar.
7
+ """
8
+
9
+ def __init__(self, route: str) -> None:
10
+ """
11
+ Inicializa a resposta de redirecionamento com a rota especificada.
12
+
13
+ Args:
14
+ route (str): A rota para a qual o chatbot deve redirecionar.
15
+ """
16
+ self.route = route
17
+
18
+
19
+ class EndChatResponse:
20
+ """
21
+ Representa uma resposta que indica o fim do chatbot.
22
+
23
+ Atributos:
24
+ end_chat_id (str): O ID do fim do chatbot.
25
+ end_chat_name (str): O nome da ação de encerramento.
26
+ observations (str): As observações finais do chatbot.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ end_chat_id: str,
32
+ end_chat_name: str = '',
33
+ observations: str = '',
34
+ ) -> None:
35
+ """
36
+ Finzaliza e tabula as informações do chatbot.
37
+ """
38
+ if not end_chat_id and not end_chat_name:
39
+ raise ValueError('end_chat_id or end_chat_name must be provided.')
40
+
41
+ self.end_chat_id = end_chat_id
42
+ self.end_chat_name = end_chat_name
43
+ self.observations = observations
44
+
45
+
46
+ class TransferToHuman:
47
+ """
48
+ Representa uma transferencia para um atendente humano.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ campaign_id: str | None = None,
54
+ campaign_name: str | None = None,
55
+ observations: str | None = None,
56
+ ) -> None:
57
+ """
58
+ Finzaliza e tabula as informações do chatbot.
59
+ """
60
+ self.campaign_id = campaign_id
61
+ self.campaign_name = campaign_name
62
+ self.observations = observations
63
+
64
+
65
+ class TransferToMenu:
66
+ """
67
+ Representa uma transferencia para outro Menu.
68
+ """
69
+
70
+ def __init__(self, menu: str, user_message: str) -> None:
71
+ """
72
+ Finzaliza e tabula as informações do chatbot.
73
+ """
74
+ self.menu = menu.lower()
75
+ self.user_message = user_message
@@ -0,0 +1,104 @@
1
+ from ..error.route_error import RouteError
2
+
3
+
4
+ class Route:
5
+ """
6
+ Representa uma rota no sistema do chatbot, gerenciando a navegação entre diferentes partes do fluxo.
7
+
8
+ Atributos:
9
+ current (str): A rota atual.
10
+ routes (list[str]): A lista de todas as rotas disponíveis no fluxo.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ current: str,
16
+ routes: list[str] | None = None,
17
+ separator: str = '.',
18
+ ):
19
+ """
20
+ Inicializa a rota com a rota atual e a lista de rotas disponíveis.
21
+
22
+ Args:
23
+ current (str): A rota atual.
24
+ routes (list[str]): A lista de todas as rotas disponíveis no fluxo.
25
+ separator (str): O separador de partes de rota. Padrão é '.'.
26
+ """
27
+ self.current = current
28
+ self.routes = routes
29
+ self.separator = separator
30
+
31
+ @property
32
+ def previous(self) -> 'Route':
33
+ """
34
+ Retorna a rota anterior a atual do usuário
35
+ """
36
+ return self.get_previous()
37
+
38
+ @property
39
+ def current_node(self) -> str:
40
+ """
41
+ Retorna o nó atual do usuário
42
+ """
43
+ return self.current.split(self.separator)[-1]
44
+
45
+ def get_previous(self) -> 'Route':
46
+ """
47
+ Retorna o caminho anterior ao caminho atual.
48
+
49
+ Raises:
50
+ RouteError: Se a rota atual for 'start', indicando que não há caminho anterior.
51
+
52
+ Returns:
53
+ str: O caminho anterior à rota atual.
54
+ """
55
+ if self.current == 'start':
56
+ return Route(self.current, self.routes, self.separator)
57
+
58
+ rotas_dedup = self.separator.join(
59
+ dict.fromkeys(self.current.split(self.separator))
60
+ )
61
+
62
+ previous_route = self.separator.join(
63
+ rotas_dedup.split(self.separator)[:-1]
64
+ )
65
+
66
+ return Route(previous_route, self.routes, self.separator)
67
+
68
+ def get_next(self, next_part: str) -> 'Route':
69
+ """
70
+ Monta e retorna o próximo caminho com base na parte fornecida.
71
+
72
+ Args:
73
+ next_part (str): A parte do caminho a ser adicionada à rota atual.
74
+
75
+ Raises:
76
+ RouteError: Se a próxima rota montada não estiver na lista de rotas disponíveis.
77
+
78
+ Returns:
79
+ Route: O próximo caminho montado.
80
+ """
81
+ next_part = next_part.strip().lower()
82
+ next_route = f'{self.current.rstrip(self.separator)}.{next_part}'
83
+ if next_part not in self.routes:
84
+ raise RouteError(f'Rota não encontrada: {next_part}')
85
+
86
+ return Route(next_route, self.routes, self.separator)
87
+
88
+ def __str__(self):
89
+ """
90
+ Retorna uma representação em string da rota atual.
91
+
92
+ Returns:
93
+ str: A representação em string da rota atual.
94
+ """
95
+ return f'Route(current={self.current})'
96
+
97
+ def __repr__(self):
98
+ """
99
+ Retorna a representação oficial da rota, que é a mesma que a representação em string.
100
+
101
+ Returns:
102
+ str: A representação oficial da rota.
103
+ """
104
+ return self.__str__()