55import json
66import os
77from dataclasses import dataclass , field
8+ from datetime import UTC , datetime , timedelta
89from pathlib import Path
910from typing import Any
1011
@@ -18,6 +19,8 @@ class ChatSession:
1819 thread_id : str
1920 title : str
2021 messages : tuple [BaseMessage , ...]
22+ created_at : datetime
23+ updated_at : datetime
2124
2225
2326@dataclass
@@ -47,17 +50,19 @@ def replace_session(
4750 title : str | None = None ,
4851 ) -> None :
4952 trimmed = list (messages [- self .max_messages :])
50- if thread_id in self . _sessions :
51- self ._sessions .pop (thread_id )
53+ now = _now ()
54+ existing = self ._sessions .get (thread_id )
5255 self ._sessions [thread_id ] = ChatSession (
5356 thread_id = thread_id ,
5457 title = title or _session_title (trimmed ),
5558 messages = tuple (trimmed ),
59+ created_at = existing .created_at if existing is not None else now ,
60+ updated_at = now ,
5661 )
5762 self ._messages = trimmed
5863
5964 def list_sessions (self , * , limit : int = 10 ) -> list [ChatSession ]:
60- sessions = list (self ._sessions .values ())
65+ sessions = sorted (self ._sessions .values (), key = lambda session : session . updated_at )
6166 return list (reversed (sessions [- limit :]))
6267
6368 def get_session (self , thread_id : str ) -> ChatSession | None :
@@ -75,6 +80,8 @@ def save(self) -> None:
7580 {
7681 "thread_id" : session .thread_id ,
7782 "title" : session .title ,
83+ "created_at" : session .created_at .isoformat (),
84+ "updated_at" : session .updated_at .isoformat (),
7885 "messages" : messages_to_dict (list (session .messages )),
7986 }
8087 for session in self ._sessions .values ()
@@ -107,22 +114,43 @@ def _load_sessions(self, raw_sessions: Any) -> None:
107114 return
108115 self ._sessions .clear ()
109116 self ._messages = []
110- for raw_session in raw_sessions :
117+ fallback_time = _now ()
118+ for index , raw_session in enumerate (raw_sessions ):
111119 if isinstance (raw_session , dict ):
112- self ._load_session (raw_session )
120+ self ._load_session (raw_session , fallback_time + timedelta ( microseconds = index ) )
113121
114- def _load_session (self , raw_session : dict [str , Any ]) -> None :
122+ def _load_session (self , raw_session : dict [str , Any ], fallback_time : datetime ) -> None :
115123 raw_thread_id = raw_session .get ("thread_id" )
116124 raw_messages = raw_session .get ("messages" )
117125 if not isinstance (raw_thread_id , str ) or not isinstance (raw_messages , list ):
118126 return
119127 messages = messages_from_dict (raw_messages )
120128 raw_title = raw_session .get ("title" )
121- self .replace_session (
122- raw_thread_id ,
123- messages ,
124- title = raw_title if isinstance (raw_title , str ) else None ,
129+ trimmed = list (messages [- self .max_messages :])
130+ created_at = _parse_time (raw_session .get ("created_at" )) or fallback_time
131+ updated_at = _parse_time (raw_session .get ("updated_at" )) or created_at
132+ self ._sessions [raw_thread_id ] = ChatSession (
133+ thread_id = raw_thread_id ,
134+ title = raw_title if isinstance (raw_title , str ) else _session_title (trimmed ),
135+ messages = tuple (trimmed ),
136+ created_at = created_at ,
137+ updated_at = updated_at ,
125138 )
139+ self ._messages = trimmed
140+
141+
142+ def _now () -> datetime :
143+ return datetime .now (UTC )
144+
145+
146+ def _parse_time (value : Any ) -> datetime | None :
147+ if not isinstance (value , str ):
148+ return None
149+ try :
150+ parsed = datetime .fromisoformat (value )
151+ except ValueError :
152+ return None
153+ return parsed if parsed .tzinfo is not None else parsed .replace (tzinfo = UTC )
126154
127155
128156def _session_title (messages : list [BaseMessage ]) -> str :
0 commit comments