feat: generate approved profile snapshots
This commit is contained in:
@@ -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]:
|
||||
"""只返回结构化候选值,不返回对话证据摘录。"""
|
||||
|
||||
Reference in New Issue
Block a user