feat: show device AI conversations
This commit is contained in:
@@ -62,6 +62,56 @@ class DeviceDAO(BaseDAO):
|
||||
)
|
||||
return result.mappings().all()
|
||||
|
||||
async def list_device_ai_conversations(
|
||||
self,
|
||||
*,
|
||||
device_id: str,
|
||||
cursor: int | None,
|
||||
limit: int,
|
||||
) -> List[Mapping[str, Any]]:
|
||||
params = {"device_id": device_id, "limit": limit + 1}
|
||||
where = "ch.device_id = :device_id"
|
||||
if cursor is not None:
|
||||
where += " AND ch.id < :cursor"
|
||||
params["cursor"] = cursor
|
||||
|
||||
result = await self.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
ch.id AS conversation_id,
|
||||
ch.role_key,
|
||||
COALESCE(r.name, ch.role_key) AS role_name,
|
||||
r.description AS role_description,
|
||||
COALESCE(stats.message_count, 0) AS message_count,
|
||||
latest.content AS last_message_preview,
|
||||
latest.created_at AS last_message_at,
|
||||
ch.last_interaction_time,
|
||||
ch.created_at,
|
||||
ch.updated_at
|
||||
FROM conversation_histories AS ch
|
||||
LEFT JOIN roles AS r
|
||||
ON r.role_key = ch.role_key
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
conversation_id,
|
||||
COUNT(*) AS message_count,
|
||||
MAX(id) AS latest_message_id
|
||||
FROM conversation_messages
|
||||
GROUP BY conversation_id
|
||||
) AS stats
|
||||
ON stats.conversation_id = ch.id
|
||||
LEFT JOIN conversation_messages AS latest
|
||||
ON latest.id = stats.latest_message_id
|
||||
WHERE {where}
|
||||
ORDER BY COALESCE(latest.created_at, ch.updated_at, ch.created_at) DESC, ch.id DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
return result.mappings().all()
|
||||
|
||||
async def get_device_status(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -51,6 +51,24 @@ class DeviceMessageListResponse(BaseModel):
|
||||
next_cursor: int | None = None
|
||||
|
||||
|
||||
class DeviceAiConversationItem(BaseModel):
|
||||
conversation_id: int
|
||||
role_key: str
|
||||
role_name: str
|
||||
role_description: str | None = None
|
||||
message_count: int
|
||||
last_message_preview: str | None = None
|
||||
last_message_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class DeviceAiConversationListResponse(BaseModel):
|
||||
items: list[DeviceAiConversationItem]
|
||||
total: int
|
||||
next_cursor: int | None = None
|
||||
|
||||
|
||||
class DeviceStatusResponse(BaseModel):
|
||||
device_id: str
|
||||
child_id: int | None = None
|
||||
@@ -159,6 +177,20 @@ def _row_to_message_item(row: Mapping) -> DeviceMessageItem:
|
||||
)
|
||||
|
||||
|
||||
def _row_to_ai_conversation_item(row: Mapping) -> DeviceAiConversationItem:
|
||||
return DeviceAiConversationItem(
|
||||
conversation_id=int(row["conversation_id"]),
|
||||
role_key=str(row["role_key"]),
|
||||
role_name=str(row["role_name"] or row["role_key"]),
|
||||
role_description=row.get("role_description"),
|
||||
message_count=int(row["message_count"] or 0),
|
||||
last_message_preview=row.get("last_message_preview"),
|
||||
last_message_at=row.get("last_message_at"),
|
||||
created_at=row["created_at"],
|
||||
updated_at=row["updated_at"],
|
||||
)
|
||||
|
||||
|
||||
def _row_to_current_location_response(row: Mapping) -> DeviceLocationCurrentResponse:
|
||||
return DeviceLocationCurrentResponse(
|
||||
child_id=int(row["child_id"]),
|
||||
@@ -293,6 +325,43 @@ async def list_device_messages(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{device_id}/ai-conversations", response_model=DeviceAiConversationListResponse)
|
||||
async def list_device_ai_conversations(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
cursor: int | None = Query(default=None, ge=1),
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
) -> DeviceAiConversationListResponse:
|
||||
rows = await device_service.list_device_ai_conversations(
|
||||
device_id=device_id,
|
||||
user_id=current_user_id,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
has_more = len(rows) > limit
|
||||
rows = rows[:limit]
|
||||
next_cursor = int(rows[-1]["conversation_id"]) if has_more and rows else None
|
||||
|
||||
logger.info(
|
||||
"listed device ai conversations",
|
||||
extra={
|
||||
"event": "device_ai_conversations",
|
||||
"request_id": getattr(request.state, "request_id", None),
|
||||
"user_id": current_user_id,
|
||||
"device_id": device_id,
|
||||
"returned_count": len(rows),
|
||||
},
|
||||
)
|
||||
|
||||
return DeviceAiConversationListResponse(
|
||||
items=[_row_to_ai_conversation_item(row) for row in rows],
|
||||
total=len(rows),
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{device_id}/status", response_model=DeviceStatusResponse)
|
||||
async def get_device_status(
|
||||
device_id: str,
|
||||
|
||||
@@ -43,6 +43,26 @@ class DeviceService(DatabaseServiceBase):
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def list_device_ai_conversations(
|
||||
self,
|
||||
*,
|
||||
device_id: str,
|
||||
user_id: int,
|
||||
cursor: int | None,
|
||||
limit: int,
|
||||
) -> List[Mapping[str, Any]]:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = DeviceDAO(db_session)
|
||||
await dao.ensure_device_access(device_id=device_id, user_id=user_id)
|
||||
return await dao.list_device_ai_conversations(
|
||||
device_id=device_id,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
)
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def get_device_status(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user