feat: persist photos and enrich conversation history

This commit is contained in:
Xin Wang
2026-08-05 16:14:03 +08:00
parent 545536bdf3
commit 317a0600bc
19 changed files with 1146 additions and 119 deletions
+89 -1
View File
@@ -9,9 +9,10 @@ from copy import deepcopy
from datetime import UTC, datetime
from uuid import uuid4
from db.models import ConversationMessage, ConversationSession
from db.models import ConversationArtifact, ConversationMessage, ConversationSession
from db.session import SessionLocal
from loguru import logger
from services.object_storage import delete_object, put_object, storage_uri
MAX_WORKFLOW_TRACE_EVENTS = 1000
@@ -36,6 +37,7 @@ class ConversationRecorder:
self._trace_sequence = 0
self._lock = asyncio.Lock()
self._seen_events: set[str] = set()
self._pending_artifacts: set[asyncio.Task[None]] = set()
@classmethod
async def start(
@@ -168,6 +170,89 @@ class ConversationRecorder:
except Exception as exc:
logger.error(f"保存对话文本失败,不影响本次通话: {exc}")
def record_image_later(
self,
data: bytes,
*,
input_id: str,
timestamp: object,
mime_type: str = "image/jpeg",
) -> None:
"""Persist a captured frame without delaying the realtime model turn."""
task = asyncio.create_task(
self._record_image(
data,
input_id=input_id,
timestamp=timestamp,
mime_type=mime_type,
),
name=f"conversation-image:{self.session_id}:{input_id}",
)
self._pending_artifacts.add(task)
task.add_done_callback(self._pending_artifacts.discard)
async def _record_image(
self,
data: bytes,
*,
input_id: str,
timestamp: object,
mime_type: str,
) -> None:
message_id = f"msg_{uuid4().hex[:20]}"
artifact_id = f"artifact_{uuid4().hex[:20]}"
extension = ".jpg" if mime_type == "image/jpeg" else ".bin"
key = f"conversations/{self.session_id}/{artifact_id}{extension}"
try:
await asyncio.to_thread(put_object, key, data, mime_type)
except Exception as exc:
logger.error(f"保存会话图片到对象存储失败,不影响本次通话: {exc}")
return
async with self._lock:
next_sequence = self._sequence + 1
try:
async with SessionLocal() as db:
db.add(
ConversationMessage(
id=message_id,
session_id=self.session_id,
sequence=next_sequence,
role="user",
content_type="image",
content="",
occurred_at=_parse_timestamp(timestamp),
extra={
"input_id": input_id,
"source": "camera_capture",
},
)
)
db.add(
ConversationArtifact(
id=artifact_id,
session_id=self.session_id,
message_id=message_id,
kind="image",
storage_uri=storage_uri(key),
mime_type=mime_type,
size_bytes=len(data),
extra={"input_id": input_id},
)
)
conversation = await db.get(ConversationSession, self.session_id)
if conversation:
conversation.message_count = next_sequence
await db.commit()
self._sequence = next_sequence
except Exception as exc:
logger.error(f"保存会话图片索引失败,不影响本次通话: {exc}")
try:
await asyncio.to_thread(delete_object, key)
except Exception:
logger.warning(f"清理未关联的会话图片失败: {key}")
async def finish(self, *, status: str = "completed") -> None:
"""Finish the session even if the owning pipeline is being cancelled."""
finish_task = asyncio.create_task(
@@ -184,6 +269,9 @@ class ConversationRecorder:
raise
async def _finish(self, *, status: str) -> None:
pending_artifacts = list(self._pending_artifacts)
if pending_artifacts:
await asyncio.gather(*pending_artifacts, return_exceptions=True)
async with self._lock:
try:
async with SessionLocal() as db: