"""为投顾 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]], }