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,26 @@
1
+ class RouteError(Exception):
2
+ """
3
+ Exceção personalizada para erros relacionados a rotas no sistema do chatbot.
4
+
5
+ Atributos:
6
+ message (str): A mensagem de erro descrevendo o problema.
7
+ """
8
+
9
+ def __init__(self, message: str):
10
+ """
11
+ Inicializa a exceção RouteError com uma mensagem de erro.
12
+
13
+ Args:
14
+ message (str): A mensagem de erro descrevendo o problema.
15
+ """
16
+ self.message = message
17
+ super().__init__(self.message)
18
+
19
+ def __str__(self):
20
+ """
21
+ Retorna a representação em string da exceção RouteError.
22
+
23
+ Returns:
24
+ str: Uma string formatada que inclui o nome da exceção e a mensagem de erro.
25
+ """
26
+ return f'RouteError: {self.message}'
@@ -0,0 +1,189 @@
1
+ import os
2
+ import grpc
3
+ import json
4
+ from rich.console import Console
5
+
6
+ import chatgraph.pb.router_pb2 as chatbot_pb2
7
+ import chatgraph.pb.router_pb2_grpc as chatbot_pb2_grpc
8
+
9
+
10
+ class RouterServiceClient:
11
+ def __init__(self, grpc_uri=None):
12
+ self.grpc_uri = grpc_uri or os.getenv("GRPC_URI")
13
+
14
+ if not self.grpc_uri:
15
+ raise ValueError("A variável de ambiente 'GRPC_URI' não está definida.")
16
+
17
+ # Cria o canal gRPC
18
+ self.channel = grpc.insecure_channel(self.grpc_uri)
19
+
20
+ # Cria os stubs para os serviços gRPC
21
+ self.user_state_stub = chatbot_pb2_grpc.UserStateServiceStub(self.channel)
22
+ self.send_message_stub = chatbot_pb2_grpc.SendMessageStub(self.channel)
23
+ self.transfer_stub = chatbot_pb2_grpc.TransferStub(self.channel)
24
+ self.end_chat_stub = chatbot_pb2_grpc.EndChatStub(self.channel)
25
+
26
+ self.console = Console()
27
+
28
+ def insert_update_user_state(self, user_state_data):
29
+ request = chatbot_pb2.UserState(**user_state_data)
30
+ try:
31
+ response = self.user_state_stub.InsertUpdateUserState(request)
32
+ if not response.status:
33
+ self.console.print(
34
+ f"Erro ao chamar InsertUpdateUserState: {response.message}",
35
+ style="bold red",
36
+ )
37
+ return response
38
+ except grpc.RpcError as e:
39
+ self.console.print(
40
+ f"Erro ao chamar InsertUpdateUserState: {e}", style="bold red"
41
+ )
42
+ return None
43
+
44
+ def delete_user_state(self, chat_id_data):
45
+ request = chatbot_pb2.ChatID(**chat_id_data)
46
+ try:
47
+ response = self.user_state_stub.DeleteUserState(request)
48
+ if not response.status:
49
+ self.console.print(
50
+ f"Erro ao chamar SendMessage: {response.message}", style="bold red"
51
+ )
52
+ return response
53
+ except grpc.RpcError as e:
54
+ self.console.print(f"Erro ao chamar DeleteUserState: {e}", style="bold red")
55
+ return None
56
+
57
+ def get_user_state(self, chat_id_data):
58
+ request = chatbot_pb2.ChatID(**chat_id_data)
59
+ try:
60
+ response = self.user_state_stub.GetUserState(request)
61
+ return response
62
+ except grpc.RpcError as e:
63
+ self.console.print(f"Erro ao chamar GetUserState: {e}", style="bold red")
64
+ return None
65
+
66
+ def send_message(self, message_data):
67
+ # print(json.dumps(message_data))
68
+
69
+ request = chatbot_pb2.Message(**message_data)
70
+
71
+ try:
72
+ response = self.send_message_stub.SendMessage(request)
73
+ if not response.status:
74
+ self.console.print(
75
+ f"Erro ao chamar SendMessage: {response.message}", style="bold red"
76
+ )
77
+ return response
78
+ except grpc.RpcError as e:
79
+ self.console.print(f"Erro ao chamar SendMessage: {e}", style="bold red")
80
+ return None
81
+
82
+ def send_image(self, message_data):
83
+ # print(json.dumps(message_data))
84
+
85
+ request = chatbot_pb2.FileMessage(**message_data)
86
+
87
+ try:
88
+ response = self.send_message_stub.SendImage(request)
89
+ if not response.status and response.message != "arquivo não encontrado":
90
+ self.console.print(
91
+ f"Erro ao chamar SendImage: {response.message}", style="bold red"
92
+ )
93
+ elif response.message == "arquivo não encontrado":
94
+ print("Arquivo não encontrado, Carregando arquivo...")
95
+ return response
96
+ except grpc.RpcError as e:
97
+ self.console.print(f"Erro ao chamar SendImage: {e}", style="bold red")
98
+ return None
99
+
100
+ def upload_file(self, file_data):
101
+ request = chatbot_pb2.UploadFileRequest(**file_data)
102
+ try:
103
+ response = self.send_message_stub.UploadFile(request)
104
+ if not response.status:
105
+ self.console.print(
106
+ f"Erro ao chamar UploadFile: {response.message}", style="bold red"
107
+ )
108
+ return response
109
+ except grpc.RpcError as e:
110
+ self.console.print(f"Erro ao chamar UploadFile: {e}", style="bold red")
111
+ return None
112
+
113
+ def transfer_to_human(self, transfer_request_data):
114
+ request = chatbot_pb2.TransferToHumanRequest(**transfer_request_data)
115
+ try:
116
+ response = self.transfer_stub.TransferToHuman(request)
117
+ if not response.status:
118
+ self.console.print(
119
+ f"Erro ao chamar SendMessage: {response.message}", style="bold red"
120
+ )
121
+ return response
122
+ except grpc.RpcError as e:
123
+ self.console.print(f"Erro ao chamar TransferToHuman: {e}", style="bold red")
124
+ return None
125
+
126
+ def transfer_to_menu(self, transfer_request_data):
127
+ request = chatbot_pb2.TransferToMenuRequest(**transfer_request_data)
128
+ try:
129
+ response = self.transfer_stub.TransferToMenu(request)
130
+ if not response.status:
131
+ self.console.print(
132
+ f"Erro ao chamar TransferToMenu: {response.message}",
133
+ style="bold red",
134
+ )
135
+ return response
136
+ except grpc.RpcError as e:
137
+ self.console.print(f"Erro ao chamar TransferToMenu: {e}", style="bold red")
138
+ return None
139
+
140
+ def end_chat(self, end_chat_request_data):
141
+ request = chatbot_pb2.EndChatRequest(**end_chat_request_data)
142
+ try:
143
+ response = self.end_chat_stub.EndChat(request)
144
+ if not response.status:
145
+ self.console.print(
146
+ f"Erro ao chamar SendMessage: {response.message}", style="bold red"
147
+ )
148
+ return response
149
+ except grpc.RpcError as e:
150
+ self.console.print(f"Erro ao chamar EndChat: {e}", style="bold red")
151
+ return None
152
+
153
+ def get_campaign_id(self, campaign_name):
154
+ request = chatbot_pb2.CampaignName(**campaign_name)
155
+ try:
156
+ response = self.transfer_stub.GetCampaignID(request)
157
+ return response
158
+ except grpc.RpcError as e:
159
+ self.console.print(f"Erro ao chamar GetCampaignID: {e}", style="bold red")
160
+ return None
161
+
162
+ def get_all_campaigns(self):
163
+ request = chatbot_pb2.Void()
164
+ try:
165
+ response = self.transfer_stub.GetAllCampaigns(request)
166
+ return response
167
+ except grpc.RpcError as e:
168
+ self.console.print(f"Erro ao chamar GetAllCampaigns: {e}", style="bold red")
169
+ return None
170
+
171
+ def get_tabulation_id(self, tabulation_name):
172
+ request = chatbot_pb2.TabulationName(**tabulation_name)
173
+ try:
174
+ response = self.end_chat_stub.GetTabulationID(request)
175
+ return response
176
+ except grpc.RpcError as e:
177
+ self.console.print(f"Erro ao chamar GetTabulationID: {e}", style="bold red")
178
+ return None
179
+
180
+ def get_all_tabulations(self):
181
+ request = chatbot_pb2.Void()
182
+ try:
183
+ response = self.end_chat_stub.GetAllTabulations(request)
184
+ return response
185
+ except grpc.RpcError as e:
186
+ self.console.print(
187
+ f"Erro ao chamar GetAllTabulations: {e}", style="bold red"
188
+ )
189
+ return None
@@ -0,0 +1,212 @@
1
+ import json
2
+ from logging import info
3
+ import os
4
+ import aio_pika
5
+ from typing import Callable
6
+ from ..auth.credentials import Credential
7
+ from ..models.message import Message
8
+ from ..models.userstate import UserState
9
+ from ..services.router_http_client import RouterHTTPClient
10
+ from ..types.usercall import UserCall
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from rich.panel import Panel
15
+ from urllib.parse import quote
16
+
17
+
18
+ class MessageConsumer:
19
+ def __init__(
20
+ self,
21
+ credential: Credential,
22
+ amqp_url: str,
23
+ router_url: str,
24
+ router_token: str,
25
+ queue_consume: str,
26
+ prefetch_count: int = 1,
27
+ virtual_host: str = '/',
28
+ ) -> None:
29
+ self.__virtual_host = virtual_host
30
+ self.__prefetch_count = prefetch_count
31
+ self.__queue_consume = queue_consume
32
+ self.__amqp_url = amqp_url
33
+ self.__router_url = router_url
34
+ self.__router_token = router_token
35
+ self.__credentials = credential
36
+ self.__router_client = None
37
+
38
+ @classmethod
39
+ def load_dotenv(
40
+ cls,
41
+ user_env: str = 'RABBIT_USER',
42
+ pass_env: str = 'RABBIT_PASS',
43
+ uri_env: str = 'RABBIT_URI',
44
+ queue_env: str = 'RABBIT_QUEUE',
45
+ prefetch_env: str = 'RABBIT_PREFETCH',
46
+ vhost_env: str = 'RABBIT_VHOST',
47
+ router_env: str = 'ROUTER_URL',
48
+ router_token_env: str = 'ROUTER_TOKEN',
49
+ ) -> 'MessageConsumer':
50
+ username = os.getenv(user_env)
51
+ password = os.getenv(pass_env)
52
+ url = os.getenv(uri_env)
53
+ queue = os.getenv(queue_env)
54
+ prefetch = os.getenv(prefetch_env, '1')
55
+ vhost = os.getenv(vhost_env, '/')
56
+ router_url = os.getenv(router_env)
57
+ router_token = os.getenv(router_token_env)
58
+
59
+ envs_essentials = {
60
+ username: user_env,
61
+ password: pass_env,
62
+ url: uri_env,
63
+ queue: queue_env,
64
+ router_url: router_env,
65
+ router_token: router_token_env,
66
+ }
67
+
68
+ if None in envs_essentials:
69
+ envs_missing = [v for k, v in envs_essentials.items() if k is None]
70
+ raise ValueError(
71
+ f'Corrija as variáveis de ambiente: {envs_missing}'
72
+ )
73
+
74
+ return cls(
75
+ credential=Credential(username=username, password=password),
76
+ amqp_url=url,
77
+ queue_consume=queue,
78
+ prefetch_count=int(prefetch),
79
+ virtual_host=vhost,
80
+ router_url=router_url,
81
+ router_token=router_token,
82
+ )
83
+
84
+ async def __initialize_router(self) -> RouterHTTPClient:
85
+ """Inicializa o cliente HTTP apenas uma vez (singleton)."""
86
+ if self.__router_client is None:
87
+ self.__router_client = RouterHTTPClient(
88
+ base_url=self.__router_url,
89
+ username='chatgraph',
90
+ password=self.__router_token,
91
+ )
92
+ return self.__router_client
93
+
94
+ async def start_consume(self, process_message: Callable):
95
+ try:
96
+ # Inicializar cliente HTTP uma única vez
97
+ await self.__initialize_router()
98
+
99
+ user = quote(self.__credentials.username)
100
+ pwd = quote(self.__credentials.password)
101
+ vhost = quote(self.__virtual_host)
102
+ uri = self.__amqp_url
103
+ amqp_url = f'amqp://{user}:{pwd}@{uri}/{vhost}'
104
+ connection = await aio_pika.connect_robust(amqp_url)
105
+
106
+ async with connection:
107
+ channel = await connection.channel()
108
+ await channel.set_qos(prefetch_count=self.__prefetch_count)
109
+
110
+ try:
111
+ queue = await channel.get_queue(
112
+ self.__queue_consume, ensure=True
113
+ )
114
+ except aio_pika.exceptions.ChannelNotFoundEntity:
115
+ arguments = {
116
+ 'x-dead-letter-exchange': 'log_error', # Dead Letter Exchange
117
+ 'x-expires': 86400000, # Expiração da fila (em milissegundos)
118
+ 'x-message-ttl': 300000, # Tempo de vida das mensagens (em milissegundos)
119
+ }
120
+ queue = await channel.declare_queue(
121
+ self.__queue_consume,
122
+ durable=True,
123
+ arguments=arguments,
124
+ )
125
+
126
+ info('[x] Server inicializado! Aguardando solicitações RPC')
127
+
128
+ async for message in queue:
129
+ async with message.process():
130
+ await self.on_request(message.body, process_message)
131
+
132
+ except Exception as e:
133
+ print(f'Erro durante o consumo de mensagens: {e}')
134
+ # Reiniciar a conexão em caso de falha
135
+ # await self.start_consume(process_message)
136
+ finally:
137
+ # Fechar cliente HTTP quando o consumer parar
138
+ await self.cleanup()
139
+
140
+ async def on_request(self, body: bytes, process_message: Callable):
141
+ try:
142
+ message = body.decode()
143
+ message_json = json.loads(message)
144
+ pure_message = await self.__transform_message(message_json)
145
+ await process_message(pure_message)
146
+ except Exception as e:
147
+ print(f'Erro ao processar mensagem: {e}')
148
+
149
+ async def __transform_message(self, message: dict) -> UserCall:
150
+ user_state = message.get('user_state', {})
151
+ message_data = message.get('message', {})
152
+ observation = user_state.get('observation', {})
153
+
154
+ if isinstance(observation, str):
155
+ observation = json.loads(observation)
156
+
157
+ user_state_models = UserState.from_dict(user_state)
158
+ message_models = Message.from_dict(message_data)
159
+
160
+ # Reutilizar o mesmo cliente para todas as mensagens
161
+ router_client = await self.__initialize_router()
162
+
163
+ usercall = UserCall(
164
+ user_state=user_state_models,
165
+ message=message_models,
166
+ router_client=router_client,
167
+ )
168
+
169
+ return usercall
170
+
171
+ async def cleanup(self):
172
+ """Libera recursos do cliente HTTP."""
173
+ if self.__router_client:
174
+ await self.__router_client.close()
175
+ self.__router_client = None
176
+ print('✓ RouterHTTPClient fechado')
177
+
178
+ def reprer(self):
179
+ console = Console()
180
+
181
+ title_text = Text('ChatGraph', style='bold red', justify='center')
182
+ title_panel = Panel.fit(
183
+ title_text, title=' ', border_style='bold red', padding=(1, 4)
184
+ )
185
+
186
+ separator = Text(
187
+ '🐇🐇🐇 RabbitMessageConsumer 📨📨📨',
188
+ style='cyan',
189
+ justify='center',
190
+ )
191
+
192
+ table = Table(
193
+ show_header=True,
194
+ header_style='bold magenta',
195
+ title='RabbitMQ Consumer',
196
+ )
197
+ table.add_column(
198
+ 'Atributo', justify='center', style='cyan', no_wrap=True
199
+ )
200
+ table.add_column('Valor', justify='center', style='magenta')
201
+
202
+ table.add_row('Virtual Host', self.__virtual_host)
203
+ table.add_row('Prefetch Count', str(self.__prefetch_count))
204
+ table.add_row('Queue Consume', self.__queue_consume)
205
+ table.add_row('AMQP URL', self.__amqp_url)
206
+ table.add_row('Rabbit Username', self.__credentials.username)
207
+ table.add_row('Rabbit Password', '******')
208
+ table.add_row('Router URL', self.__router_url)
209
+
210
+ console.print(title_panel, justify='center')
211
+ console.print(separator, justify='center')
212
+ console.print(table, justify='center')
@@ -0,0 +1,138 @@
1
+ """
2
+ Modelos de dados para ações do chatbot.
3
+
4
+ Este módulo contém as dataclasses e enums para representar ações
5
+ como transferências e encerramentos de chat.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+
11
+
12
+ class ActionType(Enum):
13
+ """
14
+ Tipo de ação no sistema.
15
+
16
+ Attributes:
17
+ TRANSFER: Transferência para atendimento humano
18
+ END_CHAT: Encerramento de chat
19
+ MESSAGE: Mensagem
20
+ """
21
+
22
+ TRANSFER = "TRANSFER"
23
+ END_CHAT = "END_CHAT"
24
+ MESSAGE = "MESSAGE"
25
+
26
+ @classmethod
27
+ def from_string(cls, value: str) -> "ActionType":
28
+ """
29
+ Cria ActionType a partir de string.
30
+
31
+ Args:
32
+ value: String representando o tipo
33
+
34
+ Returns:
35
+ ActionType correspondente
36
+
37
+ Raises:
38
+ ValueError: Se o valor não for válido
39
+ """
40
+ try:
41
+ return cls(value.upper())
42
+ except ValueError:
43
+ raise ValueError(f"invalid action type: {value}")
44
+
45
+
46
+ @dataclass
47
+ class EndAction:
48
+ """
49
+ Ação de encerramento de chat.
50
+
51
+ Attributes:
52
+ id: ID único da ação
53
+ name: Nome da ação/tabulação
54
+ department_id: ID do departamento
55
+ observation: Observação sobre o encerramento
56
+ last_update: Data/hora de última atualização (ISO 8601)
57
+ """
58
+
59
+ id: str = ""
60
+ name: str = ""
61
+ department_id: int = 0
62
+ observation: str = ""
63
+ last_update: str = ""
64
+
65
+ def is_empty(self) -> bool:
66
+ """Verifica se a ação está vazia."""
67
+ return not self.id and not self.name
68
+
69
+ def to_dict(self) -> dict:
70
+ """Converte para dicionário."""
71
+ return {
72
+ "id": self.id,
73
+ "name": self.name,
74
+ "department_id": self.department_id,
75
+ "observation": self.observation,
76
+ "last_update": self.last_update,
77
+ }
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: dict) -> "EndAction":
81
+ """Cria instância a partir de dicionário."""
82
+ return cls(
83
+ id=data.get("id", ""),
84
+ name=data.get("name", ""),
85
+ department_id=data.get("department_id", 0),
86
+ observation=data.get("observation", ""),
87
+ last_update=data.get("last_update", ""),
88
+ )
89
+
90
+
91
+ @dataclass
92
+ class TransferToHumanAction:
93
+ """
94
+ Ação de transferência para atendimento humano.
95
+
96
+ Attributes:
97
+ id: ID único da ação
98
+ name: Nome da campanha/fila
99
+ department_id: ID do departamento
100
+ observation: Observação sobre a transferência
101
+ last_update: Data/hora de última atualização (ISO 8601)
102
+ """
103
+
104
+ id: int = 0
105
+ name: str = ""
106
+ department_id: int = 0
107
+ observation: str = ""
108
+ last_update: str = ""
109
+
110
+ def is_empty(self) -> bool:
111
+ """Verifica se a ação está vazia."""
112
+ return not self.id and not self.name
113
+
114
+ def to_dict(self) -> dict:
115
+ """Converte para dicionário."""
116
+ return {
117
+ "id": self.id,
118
+ "name": self.name,
119
+ "department_id": self.department_id,
120
+ "observation": self.observation,
121
+ "last_update": self.last_update,
122
+ }
123
+
124
+ @classmethod
125
+ def from_dict(cls, data: dict) -> "TransferToHumanAction":
126
+ """Cria instância a partir de dicionário."""
127
+ return cls(
128
+ id=data.get("id", 0),
129
+ name=data.get("name", ""),
130
+ department_id=data.get("department_id", 0),
131
+ observation=data.get("observation", ""),
132
+ last_update=data.get("last_update", ""),
133
+ )
134
+
135
+
136
+ # Instâncias vazias para comparação
137
+ EMPTY_END_ACTION = EndAction()
138
+ EMPTY_TRANSFER_TO_HUMAN_ACTION = TransferToHumanAction()