Files
Mutual_Fund/service/advisor_agent/customer_context.py
T

116 lines
4.1 KiB
Python
Raw Normal View History

2026-09-14 17:47:25 +08:00
"""为投顾 Agent 组装经过裁剪的客户综合上下文。"""
from __future__ import annotations
import logging
from decimal import Decimal
from repositories.fin_customer_profile import FinCustomerProfileRepo
from repositories.fin_holdings import FinHoldingsRepo
from repositories.fin_transaction import FinTransactionRepo
MAX_HOLDINGS = 50
MAX_TRANSACTIONS = 20
MAX_MEMORIES = 10
logger = logging.getLogger("advisor.customer_context")
def _number(value):
if isinstance(value, Decimal):
return float(value)
return value
def _profile_payload(profile) -> dict:
if profile is None:
return {}
return {
"risk_level": getattr(profile, "risk_level", None),
"risk_score": getattr(profile, "risk_score", None),
"investment_experience": getattr(profile, "investment_experience", None),
"annual_income_range": getattr(profile, "annual_income_range", None),
"total_assets": _number(getattr(profile, "total_assets", None)),
"asset_allocation": getattr(profile, "asset_allocation", None),
"product_preference": getattr(profile, "product_preference", None),
"customer_level": getattr(profile, "customer_level", None),
}
def _holding_payload(holding, product) -> dict:
return {
"product_id": holding.product_id,
"product_code": getattr(product, "product_code", None),
"product_name": getattr(product, "product_name", None),
"shares": _number(holding.shares),
"cost_amount": _number(holding.cost_amount),
"current_value": _number(holding.current_value),
"profit_loss": _number(holding.profit_loss),
"profit_ratio": _number(holding.profit_ratio),
"status": holding.status,
}
def _transaction_payload(transaction) -> dict:
return {
"product_id": transaction.product_id,
"transaction_type": transaction.transaction_type,
"amount": _number(transaction.amount),
"shares": _number(transaction.shares),
"nav": _number(transaction.nav),
"status": transaction.status,
"create_time": transaction.create_time.isoformat()
if hasattr(transaction.create_time, "isoformat")
else str(transaction.create_time),
}
def _memory_payload(memory) -> dict:
return {
"tag": memory.get("tag"),
"content": str(memory.get("content") or "")[:500],
"info_type": memory.get("info_type"),
"memory_type": memory.get("memory_type"),
}
async def load_customer_context(
db,
*,
customer_id: int,
memories: list[dict] | None = None,
profile_repo_cls=FinCustomerProfileRepo,
holdings_repo_cls=FinHoldingsRepo,
transaction_repo_cls=FinTransactionRepo,
) -> dict:
"""读取并裁剪客户画像、持仓、交易和长期记忆。"""
try:
profile = await profile_repo_cls(db).get_by_customer_id(customer_id)
except Exception: # noqa: BLE001 个性化上下文故障不阻断主流程
logger.warning("customer profile context unavailable", exc_info=True)
profile = None
try:
holding_rows = await holdings_repo_cls(db).list_with_products(
customer_id, include_closed=False
)
except Exception: # noqa: BLE001 个性化上下文故障不阻断主流程
logger.warning("customer holdings context unavailable", exc_info=True)
holding_rows = []
try:
transactions = await transaction_repo_cls(db).list_recent(
customer_id, limit=MAX_TRANSACTIONS
)
except Exception: # noqa: BLE001 个性化上下文故障不阻断主流程
logger.warning("customer transaction context unavailable", exc_info=True)
transactions = []
return {
"customer_id": customer_id,
"profile": _profile_payload(profile),
"holdings": [
_holding_payload(holding, product)
for holding, product in holding_rows[:MAX_HOLDINGS]
],
"transactions": [
_transaction_payload(item) for item in transactions[:MAX_TRANSACTIONS]
],
"memories": [_memory_payload(item) for item in (memories or [])[:MAX_MEMORIES]],
}