Coverage for src/lilbee/server/routes/sessions.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-28 17:20 +0000

1"""Session routes: list, get, markdown, create, append, fork, summary, rename, delete. 

2 

3Every route requires the token, reads included: a transcript is at least as 

4personal as the memory store next door. 

5""" 

6 

7from __future__ import annotations 

8 

9from litestar import Response, delete, get, patch, post, put 

10from litestar.datastructures import ResponseHeader 

11from litestar.exceptions import NotFoundException 

12from litestar.params import FromPath 

13 

14from lilbee.data.types import MARKDOWN_MIME 

15from lilbee.server.content_disposition import CONTENT_DISPOSITION, attachment_disposition 

16from lilbee.server.handlers.sessions import ( 

17 add_session_message, 

18 claim_session, 

19 create_session, 

20 delete_session, 

21 fork_session, 

22 get_session, 

23 get_session_markdown, 

24 list_sessions, 

25 rename_session, 

26 set_session_summary, 

27) 

28from lilbee.server.models import ( 

29 SessionCreateRequest, 

30 SessionDeleteResponse, 

31 SessionDetailResponse, 

32 SessionForkRequest, 

33 SessionListResponse, 

34 SessionMessageCreateRequest, 

35 SessionRenameRequest, 

36 SessionRenameResponse, 

37 SessionSummaryRequest, 

38) 

39 

40 

41@get("/api/sessions") 

42async def sessions_list_route() -> SessionListResponse: 

43 """List saved conversations, newest first.""" 

44 return await list_sessions() 

45 

46 

47@get("/api/sessions/{session_id:str}") 

48async def session_get_route(session_id: FromPath[str]) -> SessionDetailResponse: 

49 """Return a conversation's metadata and full transcript.""" 

50 return await get_session(session_id) 

51 

52 

53@get( 

54 "/api/sessions/{session_id:str}/markdown", 

55 media_type=MARKDOWN_MIME, 

56 raises=[NotFoundException], 

57 response_headers=[ 

58 ResponseHeader( 

59 name=CONTENT_DISPOSITION, 

60 description="The export's file name: the title slug and the session id prefix.", 

61 documentation_only=True, 

62 ) 

63 ], 

64) 

65async def session_markdown_route(session_id: FromPath[str]) -> Response[str]: 

66 """Return a conversation as a markdown document, named for download.""" 

67 export = await get_session_markdown(session_id) 

68 return Response( 

69 export.markdown, 

70 media_type=MARKDOWN_MIME, 

71 headers={CONTENT_DISPOSITION: attachment_disposition(export.filename)}, 

72 ) 

73 

74 

75@post("/api/sessions") 

76async def session_create_route(data: SessionCreateRequest) -> SessionDetailResponse: 

77 """Start a new conversation.""" 

78 return await create_session(data) 

79 

80 

81@post("/api/sessions/{session_id:str}/messages") 

82async def session_add_message_route( 

83 session_id: FromPath[str], data: SessionMessageCreateRequest 

84) -> SessionDetailResponse: 

85 """Append a turn to a conversation.""" 

86 return await add_session_message(session_id, data) 

87 

88 

89@post("/api/sessions/{session_id:str}/fork") 

90async def session_fork_route( 

91 session_id: FromPath[str], data: SessionForkRequest | None = None 

92) -> SessionDetailResponse: 

93 """Start a new conversation from a copy of this one's leading messages.""" 

94 return await fork_session(session_id, data) 

95 

96 

97@post("/api/sessions/{session_id:str}/claim") 

98async def session_claim_route(session_id: FromPath[str]) -> SessionDetailResponse: 

99 """Claim a conversation for this surface so it can append.""" 

100 return await claim_session(session_id) 

101 

102 

103@put("/api/sessions/{session_id:str}/summary") 

104async def session_set_summary_route( 

105 session_id: FromPath[str], data: SessionSummaryRequest 

106) -> SessionDetailResponse: 

107 """Replace a conversation's compaction summary.""" 

108 return await set_session_summary(session_id, data) 

109 

110 

111@patch("/api/sessions/{session_id:str}") 

112async def session_rename_route( 

113 session_id: FromPath[str], data: SessionRenameRequest 

114) -> SessionRenameResponse: 

115 """Rename a conversation.""" 

116 return await rename_session(session_id, data.title) 

117 

118 

119@delete("/api/sessions/{session_id:str}", status_code=200) 

120async def session_delete_route(session_id: FromPath[str]) -> SessionDeleteResponse: 

121 """Delete a conversation.""" 

122 return await delete_session(session_id)