feat: message persistence, traceability and empty response auto-retry
This commit is contained in:
parent
16ac592855
commit
2c5e90104d
|
|
@ -680,6 +680,9 @@ def create_app(
|
||||||
from agentkit.server.routes.portal import _conversation_store
|
from agentkit.server.routes.portal import _conversation_store
|
||||||
_conversation_store.set_session_manager(app.state.session_manager)
|
_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
|
# Initialize evolution store if configured
|
||||||
if server_config and hasattr(server_config, "evolution") and server_config.evolution:
|
if server_config and hasattr(server_config, "evolution") and server_config.evolution:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,41 @@ class ConversationStore:
|
||||||
"""Set or update the session manager for persistence."""
|
"""Set or update the session manager for persistence."""
|
||||||
self._session_manager = sm
|
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:
|
def get_or_create(self, conversation_id: str | None = None) -> Conversation:
|
||||||
if conversation_id and conversation_id in self._conversations:
|
if conversation_id and conversation_id in self._conversations:
|
||||||
conv = self._conversations[conversation_id]
|
conv = self._conversations[conversation_id]
|
||||||
|
|
@ -277,6 +312,7 @@ class ChatRequest(BaseModel):
|
||||||
class ChatResponse(BaseModel):
|
class ChatResponse(BaseModel):
|
||||||
conversation_id: str
|
conversation_id: str
|
||||||
message: str
|
message: str
|
||||||
|
timestamp: str = ""
|
||||||
matched_skill: str | None = None
|
matched_skill: str | None = None
|
||||||
routing_method: str | None = None
|
routing_method: str | None = None
|
||||||
confidence: float | None = None
|
confidence: float | None = None
|
||||||
|
|
@ -459,6 +495,7 @@ async def chat(request: ChatRequest, req: Request, _auth: None = Depends(_verify
|
||||||
return ChatResponse(
|
return ChatResponse(
|
||||||
conversation_id=conv.id,
|
conversation_id=conv.id,
|
||||||
message=response_text,
|
message=response_text,
|
||||||
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||||
matched_skill=matched_skill,
|
matched_skill=matched_skill,
|
||||||
routing_method=routing_method,
|
routing_method=routing_method,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
|
@ -640,27 +677,63 @@ def _derive_conversation_title(conv: Conversation) -> str:
|
||||||
async def get_conversation(
|
async def get_conversation(
|
||||||
conversation_id: str, limit: int = 50, _auth: None = Depends(_verify_api_key)
|
conversation_id: str, limit: int = 50, _auth: None = Depends(_verify_api_key)
|
||||||
):
|
):
|
||||||
"""Get conversation history."""
|
"""Get conversation history, with fallback to SessionManager for persisted data."""
|
||||||
if conversation_id not in _conversation_store._conversations:
|
# Try in-memory first
|
||||||
raise HTTPException(status_code=404, detail=f"Conversation '{conversation_id}' not found")
|
if conversation_id in _conversation_store._conversations:
|
||||||
conv = _conversation_store._conversations[conversation_id]
|
conv = _conversation_store._conversations[conversation_id]
|
||||||
history = _conversation_store.get_history(conversation_id, limit=limit)
|
history = _conversation_store.get_history(conversation_id, limit=limit)
|
||||||
return {
|
return {
|
||||||
"id": conv.id,
|
"id": conv.id,
|
||||||
"title": _derive_conversation_title(conv),
|
"title": _derive_conversation_title(conv),
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"id": f"{conv.id}-{i}",
|
"id": f"{conv.id}-{i}",
|
||||||
"role": m.role,
|
"role": m.role,
|
||||||
"content": m.content,
|
"content": m.content,
|
||||||
"timestamp": m.timestamp.isoformat(),
|
"timestamp": m.timestamp.isoformat(),
|
||||||
"metadata": m.metadata,
|
"metadata": m.metadata,
|
||||||
}
|
}
|
||||||
for i, m in enumerate(history)
|
for i, m in enumerate(history)
|
||||||
],
|
],
|
||||||
"created_at": conv.created_at.isoformat(),
|
"created_at": conv.created_at.isoformat(),
|
||||||
"updated_at": conv.updated_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")
|
@router.websocket("/portal/ws")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue