feat: persist photos and enrich conversation history
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user