Skip to content

Commit 043ba55

Browse files
Cristhianzlautofix-ci[bot]ogabrielluiz
authored
feat: add sessions endpoint with session management enhancements (#8596)
* 📝 (monitor.py): Add endpoint to get sessions and handle session_id encoding for API requests 📝 (use-get-messages-mutation.ts): Implement a mutation function to fetch messages with query parameters and handle session_id encoding for API requests 📝 (use-get-messages-polling.ts): Ensure proper encoding of session_id for API requests in polling mutation 📝 (use-get-messages.ts): Handle session_id encoding for API requests in messages query 📝 (new-modal.tsx): Implement functions to handle session deletion and proper encoding of session_id for API requests 📝 (utils.ts): Add functions to encode, decode, validate, format, and prepare session IDs for API requests * 📝 (constants.ts): Add SESSIONS constant to API URLs for monitoring sessions 🔧 (use-delete-messages.ts): Add queryClient to UseRequestProcessor to invalidate sessions query ✨ (use-get-sessions-from-flow.ts): Introduce useGetSessionsFromFlowQuery to fetch sessions from flow 🔧 (use-rename-session.ts): Change refetchQueries to invalidateQueries for useGetSessionsFromFlowQuery 🔧 (custom-new-modal.tsx): Update import path for IOModal to playground-modal 🔧 (session-selector.tsx): Add setActiveSession function to handle setting active session 🔧 (sidebar-open-view.tsx): Add setActiveSession function to handle setting active session ♻️ (new-modal.tsx): Refactor IOModal into playground-modal and update functionality ♻️ (playground-modal.tsx): Refactor IOModal to handle playground-specific functionality ⬆️ (flowStore.ts): Add newChatOnPlayground state and setNewChatOnPlayground function ⬆️ (index.ts): Update FlowStoreType to include newChatOnPlayground and setNewChatOnPlayground * 🔧 (pyproject.toml): update testpaths to point to the correct directory for tests ✨ (test_session_endpoint.py): add unit tests for sessions endpoint with flow_id filtering ♻️ (session-selector.tsx): refactor to trim editedSession before setting it ♻️ (sidebar-open-view.tsx): refactor to set visibleSession instead of activeSession * ✨ (use-get-sessions-from-flow.ts): Always include the flow ID as the default session if it's not already present ♻️ (playground-modal.tsx): Refactor setting sessions to include currentFlowId as the default session if not present, and handle visibility of sessions more efficiently * ♻️ (use-get-messages-mutation.ts): remove unused imports and refactor code for better readability and maintainability * ✨ (test_session_endpoint.py): refactor test function names for better clarity and consistency * ✨ (create-new-session-name.ts): add function to generate a new session name based on the current date and time 🔧 (playground-modal.tsx): import createNewSessionName function to dynamically set a new session name when no session is visible * [autofix.ci] apply automated fixes * ✨ (monitor.py): rename get_sessions endpoint to get_message_sessions for clarity and consistency 🔧 (constants.ts): remove unused SESSIONS constant from API URLs 🔧 (use-delete-messages.ts): remove commented out code and unnecessary comments ✨ (use-delete-sessions.ts): add functionality to delete sessions in frontend 🔧 (use-get-sessions-from-flow.ts): update API endpoint for getting sessions to match backend changes 🔧 (playground-modal.tsx): add functionality to delete sessions and associated messages in the UI, update UI optimistically, and handle errors appropriately * [autofix.ci] apply automated fixes * 🐛 (monitor.py): Fix type hinting issue in delete_messages function 📝 (monitor.py): Add comments and improve readability in test_messages_endpoints.py 📝 (session_endpoint.py): Update endpoint paths for consistency and clarity in test_session_endpoint.py * [autofix.ci] apply automated fixes * fix: update SQL statement to use col() for session_id filtering in get_message_sessions function --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
1 parent 068db12 commit 043ba55

19 files changed

Lines changed: 672 additions & 65 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ ignore-regex = '.*(Stati Uniti|Tense=Pres).*'
233233
timeout = 120
234234
timeout_method = "signal"
235235
minversion = "6.0"
236-
testpaths = ["tests", "integration"]
236+
testpaths = ["src/backend/tests"]
237237
console_output_style = "progress"
238238
filterwarnings = ["ignore::DeprecationWarning", "ignore::ResourceWarning"]
239239
log_cli = true

src/backend/base/langflow/api/v1/monitor.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSes
4040
raise HTTPException(status_code=500, detail=str(e)) from e
4141

4242

43+
@router.get("/messages/sessions", dependencies=[Depends(get_current_active_user)])
44+
async def get_message_sessions(
45+
session: DbSession,
46+
flow_id: Annotated[UUID | None, Query()] = None,
47+
) -> list[str]:
48+
try:
49+
stmt = select(MessageTable.session_id).distinct()
50+
stmt = stmt.where(col(MessageTable.session_id).isnot(None))
51+
52+
if flow_id:
53+
stmt = stmt.where(MessageTable.flow_id == flow_id)
54+
55+
session_ids = await session.exec(stmt)
56+
return list(session_ids)
57+
except Exception as e:
58+
raise HTTPException(status_code=500, detail=str(e)) from e
59+
60+
4361
@router.get("/messages")
4462
async def get_messages(
4563
session: DbSession,
@@ -54,7 +72,10 @@ async def get_messages(
5472
if flow_id:
5573
stmt = stmt.where(MessageTable.flow_id == flow_id)
5674
if session_id:
57-
stmt = stmt.where(MessageTable.session_id == session_id)
75+
from urllib.parse import unquote
76+
77+
decoded_session_id = unquote(session_id)
78+
stmt = stmt.where(MessageTable.session_id == decoded_session_id)
5879
if sender:
5980
stmt = stmt.where(MessageTable.sender == sender)
6081
if sender_name:

src/backend/tests/unit/test_messages_endpoints.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timezone
2+
from urllib.parse import quote
23
from uuid import UUID
34

45
import pytest
@@ -32,6 +33,20 @@ async def created_messages(session): # noqa: ARG001
3233
return await aadd_messagetables(messagetables, _session)
3334

3435

36+
@pytest.fixture
37+
async def messages_with_datetime_session_id(session): # noqa: ARG001
38+
"""Create messages with datetime-like session IDs that contain characters requiring URL encoding."""
39+
datetime_session_id = "2024-01-15 10:30:45 UTC" # Contains spaces and colons
40+
async with session_scope() as _session:
41+
messages = [
42+
MessageCreate(text="Datetime message 1", sender="User", sender_name="User", session_id=datetime_session_id),
43+
MessageCreate(text="Datetime message 2", sender="AI", sender_name="AI", session_id=datetime_session_id),
44+
]
45+
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
46+
created_messages = await aadd_messagetables(messagetables, _session)
47+
return created_messages, datetime_session_id
48+
49+
3550
@pytest.mark.api_key_required
3651
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
3752
response = await client.request(
@@ -127,3 +142,96 @@ async def test_no_messages_found_with_given_session_id(client, logged_in_headers
127142

128143
assert response.status_code == 404, response.text
129144
assert response.json()["detail"] == "Not Found"
145+
146+
147+
# Test for URL-encoded datetime session ID
148+
@pytest.mark.api_key_required
149+
async def test_get_messages_with_url_encoded_datetime_session_id(
150+
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
151+
):
152+
"""Test that URL-encoded datetime session IDs are properly decoded and matched."""
153+
created_messages, datetime_session_id = messages_with_datetime_session_id
154+
155+
# URL encode the datetime session ID (spaces become %20, colons become %3A)
156+
encoded_session_id = quote(datetime_session_id)
157+
158+
# Test with URL-encoded session ID
159+
response = await client.get(
160+
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
161+
)
162+
163+
assert response.status_code == 200, response.text
164+
messages = response.json()
165+
assert len(messages) == 2
166+
167+
# Verify all messages have the correct (decoded) session ID
168+
for message in messages:
169+
assert message["session_id"] == datetime_session_id
170+
171+
# Verify message content
172+
assert messages[0]["text"] == "Datetime message 1"
173+
assert messages[1]["text"] == "Datetime message 2"
174+
175+
176+
@pytest.mark.api_key_required
177+
async def test_get_messages_with_non_encoded_datetime_session_id(
178+
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
179+
):
180+
"""Test that non-URL-encoded datetime session IDs also work correctly."""
181+
created_messages, datetime_session_id = messages_with_datetime_session_id
182+
183+
# Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings)
184+
response = await client.get(
185+
"api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
186+
)
187+
188+
assert response.status_code == 200, response.text
189+
messages = response.json()
190+
assert len(messages) == 2
191+
192+
# Verify all messages have the correct session ID
193+
for message in messages:
194+
assert message["session_id"] == datetime_session_id
195+
196+
197+
@pytest.mark.api_key_required
198+
async def test_get_messages_with_various_encoded_characters(client: AsyncClient, logged_in_headers):
199+
"""Test various URL-encoded characters in session IDs."""
200+
# Create a session ID with various special characters
201+
special_session_id = "test+session:2024@domain.com"
202+
203+
async with session_scope() as session:
204+
message = MessageCreate(
205+
text="Special chars message", sender="User", sender_name="User", session_id=special_session_id
206+
)
207+
messagetable = MessageTable.model_validate(message, from_attributes=True)
208+
await aadd_messagetables([messagetable], session)
209+
210+
# URL encode the session ID
211+
encoded_session_id = quote(special_session_id)
212+
213+
# Test with URL-encoded session ID
214+
response = await client.get(
215+
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
216+
)
217+
218+
assert response.status_code == 200, response.text
219+
messages = response.json()
220+
assert len(messages) == 1
221+
assert messages[0]["session_id"] == special_session_id
222+
assert messages[0]["text"] == "Special chars message"
223+
224+
225+
@pytest.mark.api_key_required
226+
async def test_get_messages_empty_result_with_encoded_nonexistent_session(client: AsyncClient, logged_in_headers):
227+
"""Test that URL-encoded non-existent session IDs return empty results."""
228+
nonexistent_session_id = "2024-12-31 23:59:59 UTC"
229+
encoded_session_id = quote(nonexistent_session_id)
230+
231+
response = await client.get(
232+
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
233+
)
234+
235+
assert response.status_code == 200, response.text
236+
messages = response.json()
237+
assert len(messages) == 0
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from uuid import uuid4
2+
3+
import pytest
4+
from httpx import AsyncClient
5+
from langflow.memory import aadd_messagetables
6+
from langflow.services.database.models.message.model import MessageTable
7+
from langflow.services.deps import session_scope
8+
9+
10+
@pytest.fixture
11+
async def messages_with_flow_ids(session): # noqa: ARG001
12+
"""Create messages with different session_ids and flow_ids for testing sessions endpoint."""
13+
async with session_scope() as _session:
14+
flow_id_1 = uuid4()
15+
flow_id_2 = uuid4()
16+
17+
# Create MessageTable objects directly since MessageCreate doesn't have flow_id field
18+
messagetables = [
19+
MessageTable(
20+
text="Message 1", sender="User", sender_name="User", session_id="session_A", flow_id=flow_id_1
21+
),
22+
MessageTable(text="Message 2", sender="AI", sender_name="AI", session_id="session_A", flow_id=flow_id_1),
23+
MessageTable(
24+
text="Message 3", sender="User", sender_name="User", session_id="session_B", flow_id=flow_id_1
25+
),
26+
MessageTable(
27+
text="Message 4", sender="User", sender_name="User", session_id="session_C", flow_id=flow_id_2
28+
),
29+
MessageTable(text="Message 5", sender="AI", sender_name="AI", session_id="session_D", flow_id=flow_id_2),
30+
MessageTable(
31+
text="Message 6",
32+
sender="User",
33+
sender_name="User",
34+
session_id="session_E",
35+
flow_id=None, # No flow_id
36+
),
37+
]
38+
created_messages = await aadd_messagetables(messagetables, _session)
39+
40+
return {
41+
"messages": created_messages,
42+
"flow_id_1": flow_id_1,
43+
"flow_id_2": flow_id_2,
44+
"expected_sessions_flow_1": {"session_A", "session_B"},
45+
"expected_sessions_flow_2": {"session_C", "session_D"},
46+
"expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"},
47+
}
48+
49+
50+
# Tests for /sessions endpoint
51+
@pytest.mark.api_key_required
52+
async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
53+
"""Test getting all sessions without any filter."""
54+
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
55+
56+
assert response.status_code == 200, response.text
57+
sessions = response.json()
58+
assert isinstance(sessions, list)
59+
60+
# Convert to set for easier comparison since order doesn't matter
61+
returned_sessions = set(sessions)
62+
expected_sessions = messages_with_flow_ids["expected_all_sessions"]
63+
64+
assert returned_sessions == expected_sessions
65+
assert len(sessions) == len(expected_sessions)
66+
67+
68+
@pytest.mark.api_key_required
69+
async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
70+
"""Test getting sessions filtered by flow_id."""
71+
flow_id_1 = messages_with_flow_ids["flow_id_1"]
72+
73+
response = await client.get(
74+
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers
75+
)
76+
77+
assert response.status_code == 200, response.text
78+
sessions = response.json()
79+
assert isinstance(sessions, list)
80+
81+
returned_sessions = set(sessions)
82+
expected_sessions = messages_with_flow_ids["expected_sessions_flow_1"]
83+
84+
assert returned_sessions == expected_sessions
85+
assert len(sessions) == len(expected_sessions)
86+
87+
88+
@pytest.mark.api_key_required
89+
async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
90+
"""Test getting sessions filtered by a different flow_id."""
91+
flow_id_2 = messages_with_flow_ids["flow_id_2"]
92+
93+
response = await client.get(
94+
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers
95+
)
96+
97+
assert response.status_code == 200, response.text
98+
sessions = response.json()
99+
assert isinstance(sessions, list)
100+
101+
returned_sessions = set(sessions)
102+
expected_sessions = messages_with_flow_ids["expected_sessions_flow_2"]
103+
104+
assert returned_sessions == expected_sessions
105+
assert len(sessions) == len(expected_sessions)
106+
107+
108+
@pytest.mark.api_key_required
109+
async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers):
110+
"""Test getting sessions with a non-existent flow_id returns empty list."""
111+
non_existent_flow_id = uuid4()
112+
113+
response = await client.get(
114+
"api/v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers
115+
)
116+
117+
assert response.status_code == 200, response.text
118+
sessions = response.json()
119+
assert isinstance(sessions, list)
120+
assert len(sessions) == 0
121+
122+
123+
@pytest.mark.api_key_required
124+
async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers):
125+
"""Test getting sessions when no messages exist in database."""
126+
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
127+
128+
assert response.status_code == 200, response.text
129+
sessions = response.json()
130+
assert isinstance(sessions, list)
131+
assert len(sessions) == 0
132+
133+
134+
@pytest.mark.api_key_required
135+
async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers):
136+
"""Test getting sessions with invalid flow_id format returns 422."""
137+
response = await client.get(
138+
"api/v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers
139+
)
140+
141+
assert response.status_code == 422, response.text
142+
assert "detail" in response.json()

src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const useDeleteMessages: useMutationFunctionType<
1212
undefined,
1313
DeleteMessagesParams
1414
> = (options?) => {
15-
const { mutate } = UseRequestProcessor();
15+
const { mutate, queryClient } = UseRequestProcessor();
1616

1717
const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise<any> => {
1818
const response = await api.delete(`${getURL("MESSAGES")}`, {
@@ -26,7 +26,15 @@ export const useDeleteMessages: useMutationFunctionType<
2626
DeleteMessagesParams,
2727
any,
2828
DeleteMessagesParams
29-
> = mutate(["useDeleteMessages"], deleteMessage, options);
29+
> = mutate(["useDeleteMessages"], deleteMessage, {
30+
...options,
31+
onSettled: (data, error, variables, context) => {
32+
queryClient.invalidateQueries({
33+
queryKey: ["useGetSessionsFromFlowQuery"],
34+
});
35+
options?.onSettled?.(data, error, variables, context);
36+
},
37+
});
3038

3139
return mutation;
3240
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useMutationFunctionType } from "@/types/api";
2+
import { UseMutationResult } from "@tanstack/react-query";
3+
import { api } from "../../api";
4+
import { getURL } from "../../helpers/constants";
5+
import { UseRequestProcessor } from "../../services/request-processor";
6+
7+
interface DeleteSessionParams {
8+
sessionId: string;
9+
}
10+
11+
export const useDeleteSession: useMutationFunctionType<
12+
undefined,
13+
DeleteSessionParams
14+
> = (options?) => {
15+
const { mutate, queryClient } = UseRequestProcessor();
16+
17+
const deleteSession = async ({
18+
sessionId,
19+
}: DeleteSessionParams): Promise<any> => {
20+
const response = await api.delete(
21+
`${getURL("MESSAGES")}/session/${sessionId}`,
22+
);
23+
return response.data;
24+
};
25+
26+
const mutation: UseMutationResult<
27+
DeleteSessionParams,
28+
any,
29+
DeleteSessionParams
30+
> = mutate(["useDeleteSession"], deleteSession, {
31+
...options,
32+
onSettled: (data, error, variables, context) => {
33+
queryClient.invalidateQueries({
34+
queryKey: ["useGetSessionsFromFlowQuery"],
35+
});
36+
options?.onSettled?.(data, error, variables, context);
37+
},
38+
});
39+
40+
return mutation;
41+
};

0 commit comments

Comments
 (0)