wip: 客服Agent + RAG + 画像收尾(基于 6516ccb)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""画像快照的**字段策略投影**(HTTP 端点与 Agent 工具共用的唯一实现)。
|
||||
|
||||
## 为什么单独成模块
|
||||
|
||||
投影逻辑原先写在 `app/service/public_platform_service.py`,而该模块会
|
||||
`from app.service.agent.bootstrap import get_agent_factory` ——
|
||||
一旦 `bootstrap` 反过来 import 使用投影的画像工具,就形成**循环导入**。
|
||||
把纯粹的数据投影抽到零依赖的模块,两个入口(HTTP M001/M002 与 `query_customer_profile`
|
||||
工具)共用同一份白名单,既解开环,也避免"两个入口返回不同字段"的漂移。
|
||||
|
||||
## 字段策略(`docs/05` §8.1 要求"返回经过字段策略过滤的当前画像")
|
||||
|
||||
**白名单**而非黑名单:上游快照是 JSON,新增字段若用黑名单会**自动对外可见**。
|
||||
白名单只放行**画像属性**,**不放行任何 PII** ——
|
||||
`real_name` / `birth_date` / `mobile_masked` / `trade_account` 及持仓明细都不在列内。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
#: 对外可投影的画像字段白名单。
|
||||
PROFILE_FIELD_POLICY: tuple[str, ...] = (
|
||||
"investor_type",
|
||||
"investment_horizon",
|
||||
"trading_frequency",
|
||||
"preferred_asset_class",
|
||||
"risk_tags",
|
||||
"customer_tier",
|
||||
"behavior_score",
|
||||
"total_asset",
|
||||
"assessment_valid_until",
|
||||
"assessment_expired",
|
||||
)
|
||||
|
||||
|
||||
def parse_instant(value: Any) -> datetime | None:
|
||||
"""把快照里的时间值解析成带时区的 `datetime`(不合法返回 `None`)。"""
|
||||
if isinstance(value, datetime):
|
||||
return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=UTC)
|
||||
|
||||
|
||||
def project_profile(snapshot: Any, *, now: datetime | None = None) -> dict[str, Any]:
|
||||
"""把画像快照投影成对外的 `profile` 字段(白名单 + 测评有效期**实时**判定)。
|
||||
|
||||
`assessment_expired` 按**当前时间**重算,不直接信快照里的布尔值 ——
|
||||
快照是历史的,而"测评是否仍有效"必须按当前时间算,否则过期测评会被当成有效
|
||||
(与 `SuitabilityService` 的 `ASSESSMENT_EXPIRED` 失败关闭口径一致)。
|
||||
"""
|
||||
if not isinstance(snapshot, dict):
|
||||
return {}
|
||||
current = now or datetime.now(UTC)
|
||||
projected: dict[str, Any] = {k: snapshot[k] for k in PROFILE_FIELD_POLICY if k in snapshot}
|
||||
|
||||
valid_until = parse_instant(projected.get("assessment_valid_until"))
|
||||
if valid_until is not None:
|
||||
projected["assessment_valid_until"] = valid_until.isoformat()
|
||||
projected["assessment_expired"] = valid_until <= current
|
||||
return projected
|
||||
Reference in New Issue
Block a user