@@ -241,6 +241,81 @@ async def get_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession]
241241 messages = result .scalars ().all ()
242242 return [ChatMessageModel .model_validate (message ) for message in messages ]
243243
244+ # DB column names that differ from the JSON message keys.
245+ DB_TO_JSON_KEY_MAP = {
246+ 'parent_id' : 'parentId' ,
247+ 'model_id' : 'model' ,
248+ 'status_history' : 'statusHistory' ,
249+ 'created_at' : 'timestamp' ,
250+ }
251+ # DB-internal columns excluded from the reconstructed message dict.
252+ EXCLUDED_COLUMNS = frozenset ({'id' , 'chat_id' , 'user_id' , 'updated_at' })
253+
254+ async def get_messages_map_by_chat_id (self , chat_id : str , db : Optional [AsyncSession ] = None ) -> Optional [dict ]:
255+ """Build a {message_id: message_dict} map from chat_message rows.
256+
257+ Returns the same shape as chat.history.messages so callers
258+ (get_message_list, middleware) work unchanged. Returns None if
259+ no rows exist for the chat (caller should fall back to the
260+ embedded JSON blob for legacy chats).
261+ """
262+ async with get_async_db_context (db ) as db :
263+ result = await db .execute (
264+ select (ChatMessage ).filter_by (chat_id = chat_id )
265+ )
266+ rows = result .scalars ().all ()
267+
268+ if not rows :
269+ return None
270+
271+ # Strip the composite-id prefix ("{chat_id}-") to recover the
272+ # original message_id used as map key.
273+ prefix = f'{ chat_id } -'
274+ prefix_len = len (prefix )
275+ col_keys = [c .key for c in ChatMessage .__table__ .columns ]
276+
277+ messages_map : dict [str , dict ] = {}
278+ for row in rows :
279+ msg_id = row .id [prefix_len :] if row .id .startswith (prefix ) else row .id
280+
281+ msg : dict = {'id' : msg_id }
282+ for key in col_keys :
283+ if key in self .EXCLUDED_COLUMNS :
284+ continue
285+ val = getattr (row , key )
286+ if val is None :
287+ continue
288+ json_key = self .DB_TO_JSON_KEY_MAP .get (key , key )
289+ msg [json_key ] = val
290+
291+ # Ensure content always has a value
292+ msg .setdefault ('content' , '' )
293+
294+ # Mirror usage into info.usage for callers that read it there
295+ if 'usage' in msg :
296+ msg ['info' ] = {'usage' : msg ['usage' ]}
297+
298+ messages_map [msg_id ] = msg
299+
300+ # Reconstruct childrenIds from parentId links so that the map
301+ # is fully navigable (callers like the frontend rely on this).
302+ for msg_id , msg in messages_map .items ():
303+ parent_id = msg .get ('parentId' )
304+ if parent_id and parent_id in messages_map :
305+ parent = messages_map [parent_id ]
306+ children = parent .get ('childrenIds' )
307+ if children is None :
308+ parent ['childrenIds' ] = [msg_id ]
309+ elif msg_id not in children :
310+ children .append (msg_id )
311+
312+ # Ensure every message has a childrenIds list (leaf nodes get [])
313+ for msg in messages_map .values ():
314+ if 'childrenIds' not in msg :
315+ msg ['childrenIds' ] = []
316+
317+ return messages_map
318+
244319 async def get_messages_by_user_id (
245320 self ,
246321 user_id : str ,
0 commit comments