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>
This commit is contained in:
parent
068db12c33
commit
043ba55718
19 changed files with 672 additions and 65 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
|
@ -32,6 +33,20 @@ async def created_messages(session): # noqa: ARG001
|
|||
return await aadd_messagetables(messagetables, _session)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def messages_with_datetime_session_id(session): # noqa: ARG001
|
||||
"""Create messages with datetime-like session IDs that contain characters requiring URL encoding."""
|
||||
datetime_session_id = "2024-01-15 10:30:45 UTC" # Contains spaces and colons
|
||||
async with session_scope() as _session:
|
||||
messages = [
|
||||
MessageCreate(text="Datetime message 1", sender="User", sender_name="User", session_id=datetime_session_id),
|
||||
MessageCreate(text="Datetime message 2", sender="AI", sender_name="AI", session_id=datetime_session_id),
|
||||
]
|
||||
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
|
||||
created_messages = await aadd_messagetables(messagetables, _session)
|
||||
return created_messages, datetime_session_id
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
|
||||
response = await client.request(
|
||||
|
|
@ -127,3 +142,96 @@ async def test_no_messages_found_with_given_session_id(client, logged_in_headers
|
|||
|
||||
assert response.status_code == 404, response.text
|
||||
assert response.json()["detail"] == "Not Found"
|
||||
|
||||
|
||||
# Test for URL-encoded datetime session ID
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_messages_with_url_encoded_datetime_session_id(
|
||||
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
|
||||
):
|
||||
"""Test that URL-encoded datetime session IDs are properly decoded and matched."""
|
||||
created_messages, datetime_session_id = messages_with_datetime_session_id
|
||||
|
||||
# URL encode the datetime session ID (spaces become %20, colons become %3A)
|
||||
encoded_session_id = quote(datetime_session_id)
|
||||
|
||||
# Test with URL-encoded session ID
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
messages = response.json()
|
||||
assert len(messages) == 2
|
||||
|
||||
# Verify all messages have the correct (decoded) session ID
|
||||
for message in messages:
|
||||
assert message["session_id"] == datetime_session_id
|
||||
|
||||
# Verify message content
|
||||
assert messages[0]["text"] == "Datetime message 1"
|
||||
assert messages[1]["text"] == "Datetime message 2"
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_messages_with_non_encoded_datetime_session_id(
|
||||
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
|
||||
):
|
||||
"""Test that non-URL-encoded datetime session IDs also work correctly."""
|
||||
created_messages, datetime_session_id = messages_with_datetime_session_id
|
||||
|
||||
# Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings)
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
messages = response.json()
|
||||
assert len(messages) == 2
|
||||
|
||||
# Verify all messages have the correct session ID
|
||||
for message in messages:
|
||||
assert message["session_id"] == datetime_session_id
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_messages_with_various_encoded_characters(client: AsyncClient, logged_in_headers):
|
||||
"""Test various URL-encoded characters in session IDs."""
|
||||
# Create a session ID with various special characters
|
||||
special_session_id = "test+session:2024@domain.com"
|
||||
|
||||
async with session_scope() as session:
|
||||
message = MessageCreate(
|
||||
text="Special chars message", sender="User", sender_name="User", session_id=special_session_id
|
||||
)
|
||||
messagetable = MessageTable.model_validate(message, from_attributes=True)
|
||||
await aadd_messagetables([messagetable], session)
|
||||
|
||||
# URL encode the session ID
|
||||
encoded_session_id = quote(special_session_id)
|
||||
|
||||
# Test with URL-encoded session ID
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
messages = response.json()
|
||||
assert len(messages) == 1
|
||||
assert messages[0]["session_id"] == special_session_id
|
||||
assert messages[0]["text"] == "Special chars message"
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_messages_empty_result_with_encoded_nonexistent_session(client: AsyncClient, logged_in_headers):
|
||||
"""Test that URL-encoded non-existent session IDs return empty results."""
|
||||
nonexistent_session_id = "2024-12-31 23:59:59 UTC"
|
||||
encoded_session_id = quote(nonexistent_session_id)
|
||||
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
messages = response.json()
|
||||
assert len(messages) == 0
|
||||
|
|
|
|||
142
src/backend/tests/unit/test_session_endpoint.py
Normal file
142
src/backend/tests/unit/test_session_endpoint.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from langflow.memory import aadd_messagetables
|
||||
from langflow.services.database.models.message.model import MessageTable
|
||||
from langflow.services.deps import session_scope
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def messages_with_flow_ids(session): # noqa: ARG001
|
||||
"""Create messages with different session_ids and flow_ids for testing sessions endpoint."""
|
||||
async with session_scope() as _session:
|
||||
flow_id_1 = uuid4()
|
||||
flow_id_2 = uuid4()
|
||||
|
||||
# Create MessageTable objects directly since MessageCreate doesn't have flow_id field
|
||||
messagetables = [
|
||||
MessageTable(
|
||||
text="Message 1", sender="User", sender_name="User", session_id="session_A", flow_id=flow_id_1
|
||||
),
|
||||
MessageTable(text="Message 2", sender="AI", sender_name="AI", session_id="session_A", flow_id=flow_id_1),
|
||||
MessageTable(
|
||||
text="Message 3", sender="User", sender_name="User", session_id="session_B", flow_id=flow_id_1
|
||||
),
|
||||
MessageTable(
|
||||
text="Message 4", sender="User", sender_name="User", session_id="session_C", flow_id=flow_id_2
|
||||
),
|
||||
MessageTable(text="Message 5", sender="AI", sender_name="AI", session_id="session_D", flow_id=flow_id_2),
|
||||
MessageTable(
|
||||
text="Message 6",
|
||||
sender="User",
|
||||
sender_name="User",
|
||||
session_id="session_E",
|
||||
flow_id=None, # No flow_id
|
||||
),
|
||||
]
|
||||
created_messages = await aadd_messagetables(messagetables, _session)
|
||||
|
||||
return {
|
||||
"messages": created_messages,
|
||||
"flow_id_1": flow_id_1,
|
||||
"flow_id_2": flow_id_2,
|
||||
"expected_sessions_flow_1": {"session_A", "session_B"},
|
||||
"expected_sessions_flow_2": {"session_C", "session_D"},
|
||||
"expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"},
|
||||
}
|
||||
|
||||
|
||||
# Tests for /sessions endpoint
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||
"""Test getting all sessions without any filter."""
|
||||
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
sessions = response.json()
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
# Convert to set for easier comparison since order doesn't matter
|
||||
returned_sessions = set(sessions)
|
||||
expected_sessions = messages_with_flow_ids["expected_all_sessions"]
|
||||
|
||||
assert returned_sessions == expected_sessions
|
||||
assert len(sessions) == len(expected_sessions)
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||
"""Test getting sessions filtered by flow_id."""
|
||||
flow_id_1 = messages_with_flow_ids["flow_id_1"]
|
||||
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
sessions = response.json()
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
returned_sessions = set(sessions)
|
||||
expected_sessions = messages_with_flow_ids["expected_sessions_flow_1"]
|
||||
|
||||
assert returned_sessions == expected_sessions
|
||||
assert len(sessions) == len(expected_sessions)
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||
"""Test getting sessions filtered by a different flow_id."""
|
||||
flow_id_2 = messages_with_flow_ids["flow_id_2"]
|
||||
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
sessions = response.json()
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
returned_sessions = set(sessions)
|
||||
expected_sessions = messages_with_flow_ids["expected_sessions_flow_2"]
|
||||
|
||||
assert returned_sessions == expected_sessions
|
||||
assert len(sessions) == len(expected_sessions)
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers):
|
||||
"""Test getting sessions with a non-existent flow_id returns empty list."""
|
||||
non_existent_flow_id = uuid4()
|
||||
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
sessions = response.json()
|
||||
assert isinstance(sessions, list)
|
||||
assert len(sessions) == 0
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers):
|
||||
"""Test getting sessions when no messages exist in database."""
|
||||
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
sessions = response.json()
|
||||
assert isinstance(sessions, list)
|
||||
assert len(sessions) == 0
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers):
|
||||
"""Test getting sessions with invalid flow_id format returns 422."""
|
||||
response = await client.get(
|
||||
"api/v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 422, response.text
|
||||
assert "detail" in response.json()
|
||||
Loading…
Add table
Add a link
Reference in a new issue