Skip to content

Commit 8d1225d

Browse files
Merge pull request #22 from basedosdados/dev
Update deploy workflow
2 parents 1843f1b + 81257ad commit 8d1225d

File tree

15 files changed

+2005
-1705
lines changed

15 files changed

+2005
-1705
lines changed

.github/workflows/deploy-dev.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ jobs:
7676
nginx.ingress.kubernetes.io/ssl-redirect: "true"
7777
cert-manager.io/issuer: letsencrypt-production
7878
nginx.ingress.kubernetes.io/configuration-snippet: |
79-
# Redirect exact /chatbot → /chatbot/ with a 301
80-
rewrite ^/chatbot$ /chatbot/ permanent;
79+
# Redirect exact /chatbot-streamlit → /chatbot-streamlit/ with a 301
80+
rewrite ^/chatbot-streamlit$ /chatbot-streamlit/ permanent;
8181
tls:
8282
- hosts:
8383
- development.basedosdados.org

Dockerfile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ FROM python:3.13-slim
33
WORKDIR /app/frontend
44

55
COPY . .
6-
RUN pip install --upgrade pip && pip install --no-cache-dir .
6+
7+
RUN pip install --upgrade pip && \
8+
pip install --user pipx
9+
10+
ENV PATH="/root/.local/bin:$PATH"
11+
12+
RUN pipx install poetry==2.1.3 && \
13+
poetry install --only main
714

815
EXPOSE 8501
916

10-
CMD ["bash", "-c", "streamlit run frontend/main.py"]
17+
CMD ["bash", "-c", "poetry run streamlit run frontend/main.py"]

frontend/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from .api_client import APIClient
2+
3+
__all__ = ["APIClient"]

frontend/api/api_client.py

Lines changed: 142 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import json
2+
from typing import Iterator
13
from uuid import UUID
24

3-
import requests
5+
import httpx
46
from loguru import logger
57

6-
from frontend.datatypes import MessagePair, Thread, UserMessage
8+
from frontend.datatypes import MessagePair, Step, Thread, UserMessage
79

810

911
class 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

frontend/components/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@
33
from .stylable_containers import chart_button_container, code_button_container
44
from .three_dots import three_pulsing_dots, three_waving_dots
55
from .typewriter import typewrite
6+
7+
__all__ = [
8+
"render_card",
9+
"render_disclaimer",
10+
"chart_button_container",
11+
"code_button_container",
12+
"three_pulsing_dots",
13+
"three_waving_dots",
14+
"typewrite"
15+
]

0 commit comments

Comments
 (0)