1+ import json
2+ from typing import Iterator
13from uuid import UUID
24
3- import requests
5+ import httpx
46from loguru import logger
57
6- from frontend .datatypes import MessagePair , Thread , UserMessage
8+ from frontend .datatypes import MessagePair , Step , Thread , UserMessage
79
810
911class APIClient :
@@ -12,21 +14,21 @@ def __init__(self, base_url: str):
1214 self .logger = logger .bind (classname = self .__class__ .__name__ )
1315
1416 def authenticate (self , email : str , password : str ) -> tuple [str | None , str ]:
15- """Send a post request to the authentication endpoint
17+ """Send a post request to the authentication endpoint.
1618
1719 Args:
18- email (str): The email
19- password (str): The password
20+ email (str): The email.
21+ password (str): The password.
2022
2123 Returns:
22- tuple[str|None, str]: A tuple containing the access token and a status message
24+ tuple[str|None, str]: A tuple containing the access token and a status message.
2325 """
2426 access_token = None
2527
2628 message = "Ops! Ocorreu um erro durante o login. Por favor, tente novamente."
2729
2830 try :
29- response = requests .post (
31+ response = httpx .post (
3032 url = f"{ self .base_url } /chatbot/token/" ,
3133 data = {
3234 "email" : email ,
@@ -42,104 +44,179 @@ def authenticate(self, email: str, password: str) -> tuple[str|None, str]:
4244 message = "Conectado com sucesso!"
4345 else :
4446 self .logger .error (f"[LOGIN] No access token returned" )
45- except requests . exceptions . HTTPError :
46- if response .status_code == requests .codes .unauthorized :
47+ except httpx . HTTPStatusError :
48+ if response .status_code == httpx .codes .UNAUTHORIZED :
4749 self .logger .warning (f"[LOGIN] Invalid credentials" )
4850 message = "Usuário ou senha incorretos."
4951 else :
5052 self .logger .exception (f"[LOGIN] HTTP error:" )
51- except requests . exceptions . RequestException :
53+ except Exception :
5254 self .logger .exception (f"[LOGIN] Login error:" )
5355
5456 return access_token , message
5557
56- def create_thread (self , access_token : str ) -> UUID | None :
57- """Create a thread
58+ def create_thread (self , access_token : str , title : str ) -> Thread | None :
59+ """Create a thread.
5860
5961 Args:
60- access_token (str): User access token
62+ access_token (str): User access token.
63+ title (str): The thread title.
6164
6265 Returns:
63- UUID |None: Thread unique identifier if the thread was created successfully. None otherwise
66+ Thread |None: A Thread object if the thread was created successfully. None otherwise.
6467 """
6568 self .logger .info ("[THREAD] Creating thread" )
6669
6770 try :
68- response = requests .post (
71+ response = httpx .post (
6972 url = f"{ self .base_url } /chatbot/threads/" ,
70- headers = {"Authorization" : f"Bearer { access_token } " }
73+ json = {"title" : title },
74+ headers = {"Authorization" : f"Bearer { access_token } " },
7175 )
7276 response .raise_for_status ()
7377 thread = Thread (** response .json ())
74- self .logger .success (f"[MESSAGE ] Thread created successfully for user { thread .id } " )
75- return thread . id
76- except requests . RequestException :
77- self .logger .exception (f"[MESSAGE ] Error on thread creation:" )
78+ self .logger .success (f"[THREAD ] Thread created successfully for user { thread .account } " )
79+ return thread
80+ except Exception :
81+ self .logger .exception (f"[THREAD ] Error on thread creation:" )
7882 return None
7983
80- def send_message (self , access_token : str , message : str , thread_id : UUID ) -> MessagePair :
81- """Send a user message
84+ def get_threads (self , access_token : str ) -> list [ Thread ] | None :
85+ """Get all threads from a user.
8286
8387 Args:
84- access_token (str): User access token
85- message (str): User message
86- thread_id (UUID): Thread unique identifier
88+ access_token (str): User access token.
8789
8890 Returns:
89- MessagePair:
90- A MessagePair object containing:
91- - id: unique identifier
92- - thread: thread unique identifier
93- - model_uri: assistant's model URI
94- - user_message: user message
95- - assistant_message: assistant message
96- - generated_queries: generated sql queries
97- - generated_chart: generated data for visualization
98- - created_at: message pair creation timestamp
91+ list[Thread]|None: A list of Thread objects if any thread was found. None otherwise.
92+ """
93+ self .logger .info ("[THREAD] Retrieving threads" )
94+ try :
95+ response = httpx .get (
96+ url = f"{ self .base_url } /chatbot/threads/" ,
97+ params = {"order_by" : "created_at" },
98+ headers = {"Authorization" : f"Bearer { access_token } " }
99+ )
100+ response .raise_for_status ()
101+ threads = [Thread (** thread ) for thread in response .json ()]
102+ self .logger .success (f"[THREAD] Threads retrieved successfully" )
103+ return threads
104+ except Exception :
105+ self .logger .exception (f"[THREAD] Error on threads retrieval:" )
106+ return None
107+
108+ def get_message_pairs (self , access_token : str , thread_id : UUID ) -> list [MessagePair ]| None :
109+ self .logger .info (f"[MESSAGE] Retrieving message pairs for thread { thread_id } " )
110+ try :
111+ response = httpx .get (
112+ url = f"{ self .base_url } /chatbot/threads/{ thread_id } /messages/" ,
113+ params = {"order_by" : "created_at" },
114+ headers = {"Authorization" : f"Bearer { access_token } " }
115+ )
116+ response .raise_for_status ()
117+ message_pairs = [MessagePair (** pair ) for pair in response .json ()]
118+ self .logger .success (f"[MESSAGE] Message pairs retrieved successfully for thread { thread_id } " )
119+ return message_pairs
120+ except Exception :
121+ self .logger .exception (f"[MESSAGE] Error on message pairs retrieval for thread { thread_id } :" )
122+ return None
123+
124+ def send_message (self , access_token : str , message : str , thread_id : UUID ) -> Iterator [tuple [str , Step | MessagePair ]]:
125+ """Send a user message and stream the assistant's response.
126+
127+ Args:
128+ access_token (str): The user's access token.
129+ message (str): The message sent by the user.
130+ thread_id (UUID): The unique identifier of the thread.
131+
132+ Yields:
133+ Iterator[tuple[str, Step|MessagePair]]: Tuples containing a status message and either a `Step` or `MessagePair` object.
134+ While streaming, `Step` objects are yielded. Once streaming is complete, a final `MessagePair` is yielded.
99135 """
100136 user_message = UserMessage (content = message )
101137
102138 self .logger .info (f"[MESSAGE] Sending message { user_message .id } in thread { thread_id } " )
103139
140+ steps = []
141+ error_message = None
142+ stream_completed = False
143+
104144 try :
105- response = requests .post (
145+ with httpx .stream (
146+ method = "POST" ,
106147 url = f"{ self .base_url } /chatbot/threads/{ thread_id } /messages/" ,
148+ headers = {"Authorization" : f"Bearer { access_token } " },
107149 json = user_message .model_dump (mode = "json" ),
108- headers = {"Authorization" : f"Bearer { access_token } " }
150+ timeout = httpx .Timeout (5.0 , read = 300.0 ),
151+ ) as response :
152+ response .raise_for_status ()
153+
154+ self .logger .success (f"[MESSAGE] User message sent successfully" )
155+
156+ for line in response .iter_lines ():
157+ if not line :
158+ continue
159+
160+ payload = json .loads (line )
161+ streaming_status = payload ["status" ]
162+ data = payload ["data" ]
163+
164+ if streaming_status == "running" :
165+ message = Step .model_validate_json (data )
166+ steps .append (message )
167+ elif streaming_status == "complete" :
168+ data ["steps" ] = steps
169+ message = MessagePair (** data )
170+ stream_completed = True
171+
172+ yield streaming_status , message
173+ except httpx .ReadTimeout :
174+ self .logger .exception (f"[MESSAGE] Timeout error on sending user message:" )
175+ error_message = (
176+ "Ops, parece que a solicitação expirou! Por favor, tente novamente. "
177+ "Se o problema persistir, avise-nos. Obrigado pela paciência!"
109178 )
110- response .raise_for_status ()
111- self .logger .success (f"[MESSAGE] User message sent successfully" )
112- message_pair = response .json ()
113- except requests .RequestException :
179+ except Exception :
114180 self .logger .exception (f"[MESSAGE] Error on sending user message:" )
115- message_pair = {
116- "thread" : thread_id ,
117- "model_uri" : "" ,
118- "user_message" : user_message .content ,
119- "assistant_message" : "Ops, algo deu errado! Por favor, tente novamente. " \
120- "Se o problema persistir, avise-nos. Obrigado pela paciência!"
121- }
181+ error_message = (
182+ "Ops, algo deu errado! Por favor, tente novamente. "
183+ "Se o problema persistir, avise-nos. Obrigado pela paciência!"
184+ )
122185
123- return MessagePair (** message_pair )
186+ # Safeguard for unexpected stream termination. Handles cases where the server
187+ # crashes ands the httpx.stream() call ends silently without raising an exception.
188+ if not stream_completed :
189+ if not error_message :
190+ self .logger .error ("[MESSAGE] Stream terminated without a 'complete' status" )
191+ error_message = (
192+ "Ops, a conexão com o servidor foi interrompida inesperadamente! "
193+ "Por favor, tente novamente mais tarde. Se o problema persistir, avise-nos."
194+ )
195+ message = MessagePair (
196+ user_message = user_message .content ,
197+ error_message = error_message ,
198+ steps = steps or [],
199+ )
200+ yield "complete" , message
124201
125202 def send_feedback (self , access_token : str , message_pair_id : UUID , rating : int , comments : str ) -> bool :
126- """Send a feedback
203+ """Send a feedback.
127204
128205 Args:
129- access_token (str): User access token
130- message_pair_id (UUID): The message pair unique identifier
131- rating (int): The rating (0 or 1)
132- comments (str): The comments
206+ access_token (str): User access token.
207+ message_pair_id (UUID): The message pair unique identifier.
208+ rating (int): The rating (0 or 1).
209+ comments (str): The comments.
133210
134211 Returns:
135- bool: Whether the operation succeeded or not
212+ bool: Whether the operation succeeded or not.
136213 """
137214 feedback_meaning = "positive" if rating else "negative"
138215
139216 self .logger .info (f"[FEEDBACK] Sending { feedback_meaning } feedback for message pair { message_pair_id } " )
140217
141218 try :
142- response = requests .put (
219+ response = httpx .put (
143220 url = f"{ self .base_url } /chatbot/message-pairs/{ message_pair_id } /feedbacks/" ,
144221 json = {
145222 "rating" : rating ,
@@ -150,30 +227,30 @@ def send_feedback(self, access_token: str, message_pair_id: UUID, rating: int, c
150227 response .raise_for_status ()
151228 self .logger .success (f"[FEEDBACK] Feedback sent successfully" )
152229 return True
153- except requests . exceptions . RequestException :
230+ except Exception :
154231 self .logger .exception (f"[FEEDBACK] Error on sending feedback:" )
155232 return False
156233
157- def clear_thread (self , access_token : str , thread_id : UUID ) -> bool :
158- """Clear a thread
234+ def delete_thread (self , access_token : str , thread_id : UUID ) -> bool :
235+ """Soft delete a thread and hard delete all its checkpoints.
159236
160237 Args:
161- access_token (str): User access token
162- thread_id (UUID): Thread unique identifier
238+ access_token (str): User access token.
239+ thread_id (UUID): Thread unique identifier.
163240
164241 Returns:
165- bool: Whether the operation succeeded or not
242+ bool: Whether the operation succeeded or not.
166243 """
167244 self .logger .info (f"""[CLEAR] Clearing assistant memory""" )
168245
169246 try :
170- response = requests .delete (
171- url = f"{ self .base_url } /chatbot/checkpoints /{ thread_id } /" ,
247+ response = httpx .delete (
248+ url = f"{ self .base_url } /chatbot/threads /{ thread_id } /" ,
172249 headers = {"Authorization" : f"Bearer { access_token } " }
173250 )
174251 response .raise_for_status ()
175252 self .logger .success (f"[CLEAR] Assistant memory cleared successfully" )
176253 return True
177- except requests . exceptions . RequestException :
254+ except Exception :
178255 self .logger .exception ("[CLEAR] Error on clearing assistant memory:" )
179256 return False
0 commit comments