feat:新增投顾agent和nl2sqlagent
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""从现有业务表聚合投顾意图所需的本地上下文。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from common.common_const import (
|
||||
CUSTOMER_REL_STATUS_SIGNED,
|
||||
SYS_KEY_REBALANCE_DEVIATION_THRESHOLD,
|
||||
)
|
||||
from repositories.fin_customer_profile import FinCustomerProfileRepo
|
||||
from repositories.fin_holdings import FinHoldingsRepo
|
||||
from repositories.fin_product import FinProductRepo
|
||||
from repositories.portfolio_benchmark import PortfolioBenchmarkRepo
|
||||
from repositories.risk_assessment import RiskAssessmentRepo
|
||||
from repositories.sys_config import SysConfigRepo
|
||||
from model.fin_product import FinProduct
|
||||
from service.advisor_agent.data import to_holding_input, to_product_candidate
|
||||
from service.advisor_agent.data import to_fund_performance_row
|
||||
|
||||
|
||||
async def _load_customer_risk(db, customer_id: int, profile_repo_cls, risk_repo_cls):
|
||||
assessment = await risk_repo_cls(db).get_current_by_customer(customer_id)
|
||||
if assessment is not None and assessment.risk_level:
|
||||
return assessment.risk_level
|
||||
profile = await profile_repo_cls(db).get_by_customer_id(customer_id)
|
||||
return profile.risk_level if profile is not None else None
|
||||
|
||||
|
||||
async def load_customer_risk(
|
||||
db,
|
||||
*,
|
||||
customer_id: int,
|
||||
profile_repo_cls=FinCustomerProfileRepo,
|
||||
risk_repo_cls=RiskAssessmentRepo,
|
||||
) -> str | None:
|
||||
return await _load_customer_risk(db, customer_id, profile_repo_cls, risk_repo_cls)
|
||||
|
||||
|
||||
async def load_rebalance_context(
|
||||
db,
|
||||
*,
|
||||
customer_id: int,
|
||||
profile_repo_cls=FinCustomerProfileRepo,
|
||||
risk_repo_cls=RiskAssessmentRepo,
|
||||
holdings_repo_cls=FinHoldingsRepo,
|
||||
product_repo_cls=FinProductRepo,
|
||||
benchmark_repo_cls=PortfolioBenchmarkRepo,
|
||||
sys_config_repo_cls=SysConfigRepo,
|
||||
) -> dict | None:
|
||||
"""聚合画像、持仓、在售产品和组合基准,供调仓引擎使用。
|
||||
|
||||
关系签约状态由调用方读取并校验,本函数只负责客户投资数据。
|
||||
"""
|
||||
customer_risk = await _load_customer_risk(
|
||||
db, customer_id, profile_repo_cls, risk_repo_cls
|
||||
)
|
||||
if not customer_risk:
|
||||
return None
|
||||
|
||||
benchmark = await benchmark_repo_cls(db).get_active_by_risk(customer_risk)
|
||||
if benchmark is None:
|
||||
return None
|
||||
|
||||
threshold = benchmark.drift_threshold
|
||||
if threshold is None:
|
||||
raw_threshold = await sys_config_repo_cls(db).get_value(
|
||||
SYS_KEY_REBALANCE_DEVIATION_THRESHOLD
|
||||
)
|
||||
try:
|
||||
threshold = Decimal(str(raw_threshold))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
return None
|
||||
if not threshold.is_finite() or threshold < 0:
|
||||
return None
|
||||
|
||||
product_repo = product_repo_cls(db)
|
||||
products = await product_repo.list(
|
||||
where=[FinProduct.status == "在售"],
|
||||
limit=1000,
|
||||
)
|
||||
products_by_id = {product.id: product for product in products}
|
||||
|
||||
holdings = await holdings_repo_cls(db).list_by_customer(customer_id, status="持有中")
|
||||
holding_inputs = []
|
||||
for holding in holdings:
|
||||
product = products_by_id.get(holding.product_id)
|
||||
if product is None:
|
||||
product = await product_repo.get(holding.product_id)
|
||||
if product is not None:
|
||||
holding_inputs.append(to_holding_input(holding, product))
|
||||
|
||||
return {
|
||||
"customer_id": customer_id,
|
||||
"customer_risk": customer_risk,
|
||||
"relation_status": CUSTOMER_REL_STATUS_SIGNED,
|
||||
"holdings": holding_inputs,
|
||||
"target_allocation": benchmark.target_allocation,
|
||||
"threshold": threshold,
|
||||
"candidates": [to_product_candidate(product) for product in products],
|
||||
}
|
||||
|
||||
|
||||
async def load_fund_analysis_context(
|
||||
db,
|
||||
*,
|
||||
fund_codes: list[str],
|
||||
product_repo_cls=FinProductRepo,
|
||||
performance_repo_cls=None,
|
||||
) -> list[dict]:
|
||||
if performance_repo_cls is None:
|
||||
from repositories.fund_performance import FundPerformanceRepo
|
||||
|
||||
performance_repo_cls = FundPerformanceRepo
|
||||
|
||||
product_repo = product_repo_cls(db)
|
||||
performance_repo = performance_repo_cls(db)
|
||||
result = []
|
||||
for code in fund_codes:
|
||||
product = await product_repo.get_by_code(code)
|
||||
if product is None:
|
||||
continue
|
||||
performance = await performance_repo.list_for_product(product.id)
|
||||
result.append(
|
||||
{
|
||||
"fund": {
|
||||
"fund_code": product.product_code,
|
||||
"fund_name": product.product_name,
|
||||
"risk_level": product.risk_level,
|
||||
},
|
||||
"performance": [to_fund_performance_row(row) for row in performance],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def load_recommendation_context(
|
||||
db,
|
||||
*,
|
||||
customer_id: int,
|
||||
profile_repo_cls=FinCustomerProfileRepo,
|
||||
product_repo_cls=FinProductRepo,
|
||||
performance_repo_cls=None,
|
||||
risk_repo_cls=RiskAssessmentRepo,
|
||||
) -> dict | None:
|
||||
customer_risk = await _load_customer_risk(
|
||||
db, customer_id, profile_repo_cls, risk_repo_cls
|
||||
)
|
||||
if not customer_risk:
|
||||
return None
|
||||
|
||||
products = await product_repo_cls(db).list(
|
||||
where=[FinProduct.status == "在售"],
|
||||
limit=1000,
|
||||
)
|
||||
if performance_repo_cls is None:
|
||||
from repositories.fund_performance import FundPerformanceRepo
|
||||
|
||||
performance_repo_cls = FundPerformanceRepo
|
||||
performance_repo = performance_repo_cls(db)
|
||||
candidates = []
|
||||
for product in products:
|
||||
candidate = to_product_candidate(product)
|
||||
latest = None
|
||||
if hasattr(performance_repo, "get_latest_for_product"):
|
||||
latest = await performance_repo.get_latest_for_product(product.id)
|
||||
else:
|
||||
rows = await performance_repo.list_for_product(product.id)
|
||||
latest = rows[-1] if rows else None
|
||||
return_rate = getattr(latest, "return_rate", None)
|
||||
if return_rate is not None:
|
||||
candidate["performance_score"] = float(return_rate)
|
||||
candidates.append(candidate)
|
||||
return {
|
||||
"customer_id": customer_id,
|
||||
"customer_risk": customer_risk,
|
||||
"candidates": candidates,
|
||||
}
|
||||
Reference in New Issue
Block a user