feat: generate approved profile snapshots

This commit is contained in:
张胜宇
2026-09-11 17:58:46 +08:00
parent a769130658
commit 27174b9ce2
4 changed files with 128 additions and 6 deletions
+19
View File
@@ -75,6 +75,25 @@ class MemorySyncOutbox(Base):
processed_at: Mapped[datetime | None] = mapped_column(DateTime)
class ProfileSnapshot(Base):
"""客户画像版本快照;只有审核通过的候选才能生成当前版本。"""
__tablename__ = "profile_snapshots"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
profile_uuid: Mapped[str | None] = mapped_column(String(36), unique=True)
customer_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
version: Mapped[int] = mapped_column(BigInteger, nullable=False)
snapshot: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
generation_basis: Mapped[dict[str, Any] | None] = mapped_column(JSON)
snapshot_hash: Mapped[str | None] = mapped_column(String(64))
is_current: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
current_customer_id: Mapped[int | None] = mapped_column(BigInteger, unique=True)
generated_at: Mapped[datetime | None] = mapped_column(DateTime)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class MemoryConflict(Base):
"""记忆冲突;库列名为 `status`,`resolution`/`severity`/`resolved_by` 必须映射。"""
@@ -4,8 +4,11 @@
调用权限。用户确认只把候选标记为 ``verified``,管理员批准后才切换为 ``active``。
"""
import hashlib
import json
from datetime import UTC, datetime
from typing import Any, Literal
from uuid import uuid4
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +17,7 @@ from app.core.contracts import RequestContext
from app.core.errors import GenericResourceNotFoundError, InvalidStateError
from app.infrastructure.db import SessionFactory
from app.model.audit import InteractionAudit
from app.model.memory import MemoryConflict, MemoryUnit
from app.model.memory import MemoryConflict, MemorySyncOutbox, MemoryUnit, ProfileSnapshot
from app.service.agent.bootstrap import get_memory_cache_adapter
from app.service.authorization_service import AuthorizationService
from app.service.memory_service import MemoryService
@@ -142,10 +145,69 @@ class CustomerProfileCandidateService:
candidate.status = "active"
candidate.promoted_at = now
candidate.version += 1
await self._write_profile_snapshot(session, candidate, reviewer_id, now)
await MemoryService(session, cache=get_memory_cache_adapter()).invalidate_recall_cache(
int(candidate.customer_id)
)
async def _write_profile_snapshot(
self, session: AsyncSession, candidate: MemoryUnit, reviewer_id: int, now: datetime
) -> None:
"""生成新的当前画像版本,并为两个派生存储写入可靠同步事件。"""
current = await session.scalar(
select(ProfileSnapshot)
.where(
ProfileSnapshot.customer_id == candidate.customer_id,
ProfileSnapshot.current_customer_id == candidate.customer_id,
)
.with_for_update()
)
previous = dict(current.snapshot) if current is not None else {}
preferences = dict(previous.get("customer_service_preferences", {}))
preferences[candidate.memory_key] = {
"value": candidate.content,
"memory_type": candidate.memory_type,
"confidence": float(candidate.confidence),
}
snapshot = {**previous, "customer_service_preferences": preferences}
version = (int(current.version) + 1) if current is not None else 1
profile_uuid = str(uuid4())
snapshot_hash = hashlib.sha256(
json.dumps(snapshot, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
.encode("utf-8")
).hexdigest()
if current is not None:
current.is_current = 0
current.current_customer_id = None
current.updated_at = now
created = ProfileSnapshot(
profile_uuid=profile_uuid, customer_id=candidate.customer_id,
version=version, snapshot=snapshot,
generation_basis={
"source": "customer_profile_candidate",
"candidate_id": candidate.id,
"reviewer_id": reviewer_id,
},
snapshot_hash=snapshot_hash, is_current=1,
current_customer_id=candidate.customer_id, generated_at=now,
created_at=now, updated_at=now,
)
session.add(created)
await session.flush()
for target_store in ("milvus", "neo4j"):
session.add(MemorySyncOutbox(
event_uuid=str(uuid4()), aggregate_type="profile_snapshot",
aggregate_uuid=profile_uuid, aggregate_version=version,
target_store=target_store, operation="upsert",
payload={
"customer_id": candidate.customer_id,
"profile_uuid": profile_uuid,
"profile_version": version,
"snapshot": snapshot,
},
status="pending", retry_count=0, created_at=now,
))
@staticmethod
def _view(item: MemoryUnit) -> dict[str, Any]:
"""只返回结构化候选值,不返回对话证据摘录。"""