Skip to content

Commit 27ae223

Browse files
committed
session page support ui & backend
1 parent 25d0806 commit 27ae223

File tree

13 files changed

+1419
-15
lines changed

13 files changed

+1419
-15
lines changed

backend/rest/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from rest.routers.internal import router as internal_router
2020
from rest.routers.public.traces import router as public_traces_router
21+
from rest.routers.sessions import router as sessions_router
2122
from rest.routers.traces import router as traces_router
2223
from rest.routers.users import router as users_router
2324
from rest.schemas.common import HealthResponse
@@ -41,6 +42,7 @@
4142
# Trace reading from ClickHouse (user auth via headers from Next.js)
4243
app.include_router(traces_router, prefix="/api/v1")
4344
app.include_router(users_router, prefix="/api/v1")
45+
app.include_router(sessions_router, prefix="/api/v1")
4446

4547
# Public API for SDK ingestion (API key auth)
4648
app.include_router(public_traces_router, prefix="/api/v1")

backend/rest/routers/sessions.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Session query endpoints (user-authenticated, not public API)."""
2+
3+
import logging
4+
from datetime import datetime
5+
6+
from fastapi import APIRouter, HTTPException, Query, status
7+
8+
from rest.routers.deps import ProjectAccess
9+
from rest.schemas.sessions import SessionDetailResponse, SessionListResponse
10+
from rest.services.trace_reader import get_trace_reader_service
11+
12+
logger = logging.getLogger(__name__)
13+
14+
router = APIRouter(prefix="/projects/{project_id}/sessions", tags=["Sessions"])
15+
16+
17+
@router.get("", response_model=SessionListResponse)
18+
async def list_sessions(
19+
project_id: str,
20+
_access: ProjectAccess,
21+
page: int = Query(0, ge=0, description="Page number (0-indexed)"),
22+
limit: int = Query(50, ge=1, le=200, description="Items per page"),
23+
search_query: str | None = Query(None, description="Search by session_id"),
24+
start_after: datetime | None = Query(None, description="Filter traces after this time"),
25+
end_before: datetime | None = Query(None, description="Filter traces before this time"),
26+
):
27+
"""List unique sessions for a project with trace counts and token totals."""
28+
try:
29+
service = get_trace_reader_service()
30+
result = service.list_sessions(
31+
project_id=project_id,
32+
page=page,
33+
limit=limit,
34+
search_query=search_query,
35+
start_after=start_after,
36+
end_before=end_before,
37+
)
38+
return result
39+
except Exception as e:
40+
logger.exception(f"Error listing sessions: {e}")
41+
raise HTTPException(
42+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
43+
detail="Failed to list sessions",
44+
) from e
45+
46+
47+
@router.get("/{session_id}", response_model=SessionDetailResponse)
48+
async def get_session(
49+
project_id: str,
50+
session_id: str,
51+
_access: ProjectAccess,
52+
):
53+
"""Get session detail with all traces for conversation view."""
54+
try:
55+
service = get_trace_reader_service()
56+
result = service.get_session(project_id=project_id, session_id=session_id)
57+
if result is None:
58+
raise HTTPException(
59+
status_code=status.HTTP_404_NOT_FOUND,
60+
detail="Session not found",
61+
)
62+
return result
63+
except HTTPException:
64+
raise
65+
except Exception as e:
66+
logger.exception(f"Error getting session: {e}")
67+
raise HTTPException(
68+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
69+
detail="Failed to get session",
70+
) from e

backend/rest/schemas/sessions.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Session-related response schemas."""
2+
3+
from datetime import datetime
4+
5+
from pydantic import BaseModel
6+
7+
from rest.schemas.common import PaginationMeta
8+
9+
10+
class SessionListItem(BaseModel):
11+
"""Single session with aggregated trace statistics."""
12+
13+
session_id: str
14+
trace_count: int
15+
user_ids: list[str]
16+
first_trace_time: datetime | None
17+
last_trace_time: datetime | None
18+
duration_ms: float | None
19+
total_input_tokens: int | None
20+
total_output_tokens: int | None
21+
input: str | None
22+
output: str | None
23+
24+
25+
class SessionListResponse(BaseModel):
26+
"""Paginated list of sessions."""
27+
28+
data: list[SessionListItem]
29+
meta: PaginationMeta
30+
31+
32+
class SessionTraceItem(BaseModel):
33+
"""Single trace within a session, for conversation view."""
34+
35+
trace_id: str
36+
name: str
37+
trace_start_time: datetime
38+
user_id: str | None
39+
input: str | None
40+
output: str | None
41+
duration_ms: float | None
42+
status: str
43+
44+
45+
class SessionDetailResponse(BaseModel):
46+
"""Session detail with all traces for conversation view."""
47+
48+
session_id: str
49+
traces: list[SessionTraceItem]
50+
user_ids: list[str]
51+
trace_count: int
52+
first_trace_time: datetime | None
53+
last_trace_time: datetime | None
54+
duration_ms: float | None
55+
total_input_tokens: int | None
56+
total_output_tokens: int | None

0 commit comments

Comments
 (0)