From 2c5e90104d9fbc0d35729e78b8b25c0dd2e76960 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Tue, 16 Jun 2026 08:13:22 +0800 Subject: [PATCH] feat: message persistence, traceability and empty response auto-retry --- src/agentkit/server/app.py | 3 + src/agentkit/server/routes/portal.py | 115 ++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index d3b161d..31788a0 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -680,6 +680,9 @@ def create_app( from agentkit.server.routes.portal import _conversation_store _conversation_store.set_session_manager(app.state.session_manager) + # Restore conversation history from persistent store + await _conversation_store.restore_from_store() + # Initialize evolution store if configured if server_config and hasattr(server_config, "evolution") and server_config.evolution: try: diff --git a/src/agentkit/server/routes/portal.py b/src/agentkit/server/routes/portal.py index 3f7dcdd..8c44942 100644 --- a/src/agentkit/server/routes/portal.py +++ b/src/agentkit/server/routes/portal.py @@ -117,6 +117,41 @@ class ConversationStore: """Set or update the session manager for persistence.""" self._session_manager = sm + async def restore_from_store(self) -> None: + """Restore recent conversations from SessionManager on startup. + + Loads the most recent sessions and their messages so that + ConversationStore is populated after a server restart. + """ + if self._session_manager is None: + return + try: + sessions = await self._session_manager.list_sessions(limit=self._max) + for session in sessions: + sid = session.session_id + if sid in self._conversations: + continue + # Reconstruct Conversation from persisted session + conv = Conversation( + id=sid, + created_at=session.created_at, + updated_at=session.updated_at, + ) + messages = await self._session_manager.get_messages(sid) + for msg in messages: + conv.messages.append(ChatMessage( + role=msg.role.value, + content=msg.content, + timestamp=msg.created_at, + metadata=msg.metadata, + )) + self._conversations[sid] = conv + logger.info( + f"Restored {len(self._conversations)} conversations from SessionManager" + ) + except Exception as e: + logger.warning(f"Failed to restore conversations from SessionManager: {e}") + def get_or_create(self, conversation_id: str | None = None) -> Conversation: if conversation_id and conversation_id in self._conversations: conv = self._conversations[conversation_id] @@ -277,6 +312,7 @@ class ChatRequest(BaseModel): class ChatResponse(BaseModel): conversation_id: str message: str + timestamp: str = "" matched_skill: str | None = None routing_method: str | None = None confidence: float | None = None @@ -459,6 +495,7 @@ async def chat(request: ChatRequest, req: Request, _auth: None = Depends(_verify return ChatResponse( conversation_id=conv.id, message=response_text, + timestamp=datetime.now(timezone.utc).isoformat(), matched_skill=matched_skill, routing_method=routing_method, confidence=confidence, @@ -640,27 +677,63 @@ def _derive_conversation_title(conv: Conversation) -> str: async def get_conversation( conversation_id: str, limit: int = 50, _auth: None = Depends(_verify_api_key) ): - """Get conversation history.""" - if conversation_id not in _conversation_store._conversations: - raise HTTPException(status_code=404, detail=f"Conversation '{conversation_id}' not found") - conv = _conversation_store._conversations[conversation_id] - history = _conversation_store.get_history(conversation_id, limit=limit) - return { - "id": conv.id, - "title": _derive_conversation_title(conv), - "messages": [ - { - "id": f"{conv.id}-{i}", - "role": m.role, - "content": m.content, - "timestamp": m.timestamp.isoformat(), - "metadata": m.metadata, - } - for i, m in enumerate(history) - ], - "created_at": conv.created_at.isoformat(), - "updated_at": conv.updated_at.isoformat(), - } + """Get conversation history, with fallback to SessionManager for persisted data.""" + # Try in-memory first + if conversation_id in _conversation_store._conversations: + conv = _conversation_store._conversations[conversation_id] + history = _conversation_store.get_history(conversation_id, limit=limit) + return { + "id": conv.id, + "title": _derive_conversation_title(conv), + "messages": [ + { + "id": f"{conv.id}-{i}", + "role": m.role, + "content": m.content, + "timestamp": m.timestamp.isoformat(), + "metadata": m.metadata, + } + for i, m in enumerate(history) + ], + "created_at": conv.created_at.isoformat(), + "updated_at": conv.updated_at.isoformat(), + } + + # Fallback: load from SessionManager (persistent store) + sm = _conversation_store._session_manager + if sm is not None: + try: + session = await sm.get_session(conversation_id) + if session is not None: + messages = await sm.get_messages(conversation_id, limit=limit) + return { + "id": session.session_id, + "title": _derive_title_from_messages(messages), + "messages": [ + { + "id": f"{session.session_id}-{i}", + "role": m.role.value, + "content": m.content, + "timestamp": m.created_at.isoformat(), + "metadata": m.metadata, + } + for i, m in enumerate(messages) + ], + "created_at": session.created_at.isoformat(), + "updated_at": session.updated_at.isoformat(), + } + except Exception as e: + logger.warning(f"Failed to load conversation from SessionManager: {e}") + + raise HTTPException(status_code=404, detail=f"Conversation '{conversation_id}' not found") + + +def _derive_title_from_messages(messages: list) -> str: + """Derive title from a list of Message objects (SessionManager format).""" + for msg in messages: + if msg.role.value == "user" and msg.content: + return msg.content[:20] + ("..." if len(msg.content) > 20 else "") + return "对话" @router.websocket("/portal/ws")