merge: integrate advisor capabilities on latest qyqy base
This commit is contained in:
@@ -17,35 +17,84 @@ from app.api.schemas.admin import (
|
||||
ReviewPayload,
|
||||
RoutingPayload,
|
||||
)
|
||||
from app.core.advisor_backtest_contracts import AllocationBacktestQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.core.profile_governance_contracts import ProfileDriftReviewRequest
|
||||
from app.service.admin_service import AdminService
|
||||
from app.service.allocation_backtest_service import AllocationBacktestService
|
||||
from app.service.profile_governance_service import ProfileGovernanceService
|
||||
|
||||
router = APIRouter(prefix="/api/v1/admin", tags=["platform-admin"],
|
||||
dependencies=[Depends(enforce_rate_limit)])
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/admin", tags=["platform-admin"], dependencies=[Depends(enforce_rate_limit)]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/advisor/asset-allocation-backtests", status_code=201)
|
||||
async def run_asset_allocation_backtest(
|
||||
payload: AllocationBacktestQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await AllocationBacktestService().run(payload, context, key)
|
||||
|
||||
|
||||
@router.get("/advisor/profile-tags")
|
||||
async def list_profile_tags(
|
||||
customer_id: int = Query(gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await ProfileGovernanceService().tags(customer_id, context)
|
||||
|
||||
|
||||
@router.get("/advisor/profile-drift-reviews")
|
||||
async def list_profile_drift_reviews(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await ProfileGovernanceService().pending_reviews(context)
|
||||
|
||||
|
||||
@router.post("/advisor/profile-drift-reviews/{review_id}/reviews")
|
||||
async def review_profile_drift(
|
||||
payload: ProfileDriftReviewRequest,
|
||||
review_id: int = Path(gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await ProfileGovernanceService().review(review_id, payload, context, key)
|
||||
|
||||
|
||||
def register_resource(
|
||||
resource: str, schema: type[BaseModel], id_name: str, *, scoped: bool = False,
|
||||
update: bool = True, detail: bool = True,
|
||||
resource: str,
|
||||
schema: type[BaseModel],
|
||||
id_name: str,
|
||||
*,
|
||||
scoped: bool = False,
|
||||
update: bool = True,
|
||||
detail: bool = True,
|
||||
) -> None:
|
||||
prefix = f"/config-releases/{{release_id}}/{resource}" if scoped else f"/{resource}"
|
||||
|
||||
async def create(
|
||||
payload: BaseModel, response: Response, release_id: int | None = None,
|
||||
payload: BaseModel,
|
||||
response: Response,
|
||||
release_id: int | None = None,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, Any]:
|
||||
result = await AdminService().mutate(resource, context, payload.model_dump(mode="json"),
|
||||
key, None, release_id=release_id)
|
||||
result = await AdminService().mutate(
|
||||
resource, context, payload.model_dump(mode="json"), key, None, release_id=release_id
|
||||
)
|
||||
response.headers["ETag"] = f'"{result["meta"]["etag"]}"'
|
||||
return result
|
||||
|
||||
create.__annotations__["payload"] = schema
|
||||
router.add_api_route(prefix, create, methods=["POST"], status_code=201,
|
||||
operation_id=f"create_{resource}")
|
||||
router.add_api_route(
|
||||
prefix, create, methods=["POST"], status_code=201, operation_id=f"create_{resource}"
|
||||
)
|
||||
|
||||
async def list_rows(
|
||||
release_id: int | None = None, limit: int = Query(default=20, ge=1, le=100),
|
||||
release_id: int | None = None,
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
cursor: str | None = Query(default=None),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, Any]:
|
||||
@@ -57,7 +106,8 @@ def register_resource(
|
||||
router.add_api_route(prefix, list_rows, methods=["GET"], operation_id=f"list_{resource}")
|
||||
|
||||
async def get(
|
||||
response: Response, row_id: int = Path(alias=id_name, gt=0),
|
||||
response: Response,
|
||||
row_id: int = Path(alias=id_name, gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, Any]:
|
||||
result = await AdminService().query(resource, context, row_id=row_id)
|
||||
@@ -65,43 +115,67 @@ def register_resource(
|
||||
return result
|
||||
|
||||
if detail:
|
||||
router.add_api_route(f"{prefix}/{{{id_name}}}", get, methods=["GET"],
|
||||
operation_id=f"get_{resource}")
|
||||
router.add_api_route(
|
||||
f"{prefix}/{{{id_name}}}", get, methods=["GET"], operation_id=f"get_{resource}"
|
||||
)
|
||||
|
||||
async def put(
|
||||
payload: BaseModel, response: Response, row_id: int = Path(alias=id_name, gt=0),
|
||||
payload: BaseModel,
|
||||
response: Response,
|
||||
row_id: int = Path(alias=id_name, gt=0),
|
||||
release_id: int | None = None,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
if_match: str | None = Header(default=None, alias="If-Match"),
|
||||
) -> dict[str, Any]:
|
||||
result = await AdminService().mutate(resource, context, payload.model_dump(mode="json"),
|
||||
key, if_match, row_id=row_id, release_id=release_id)
|
||||
result = await AdminService().mutate(
|
||||
resource,
|
||||
context,
|
||||
payload.model_dump(mode="json"),
|
||||
key,
|
||||
if_match,
|
||||
row_id=row_id,
|
||||
release_id=release_id,
|
||||
)
|
||||
response.headers["ETag"] = f'"{result["meta"]["etag"]}"'
|
||||
return result
|
||||
|
||||
put.__annotations__["payload"] = schema
|
||||
if update:
|
||||
router.add_api_route(f"{prefix}/{{{id_name}}}", put, methods=["PUT"],
|
||||
operation_id=f"update_{resource}")
|
||||
router.add_api_route(
|
||||
f"{prefix}/{{{id_name}}}", put, methods=["PUT"], operation_id=f"update_{resource}"
|
||||
)
|
||||
|
||||
|
||||
def register_transition(resource: str, id_name: str, action: str) -> None:
|
||||
async def transition(
|
||||
payload: BaseModel, response: Response, row_id: int = Path(alias=id_name, gt=0),
|
||||
payload: BaseModel,
|
||||
response: Response,
|
||||
row_id: int = Path(alias=id_name, gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
if_match: str | None = Header(default=None, alias="If-Match"),
|
||||
) -> dict[str, Any]:
|
||||
result = await AdminService().mutate(resource, context, payload.model_dump(mode="json"),
|
||||
key, if_match, row_id=row_id, action=action)
|
||||
result = await AdminService().mutate(
|
||||
resource,
|
||||
context,
|
||||
payload.model_dump(mode="json"),
|
||||
key,
|
||||
if_match,
|
||||
row_id=row_id,
|
||||
action=action,
|
||||
)
|
||||
response.headers["ETag"] = f'"{result["meta"]["etag"]}"'
|
||||
return result
|
||||
|
||||
transition.__annotations__["payload"] = ReviewPayload if action == "reviews" else EmptyPayload
|
||||
router.add_api_route(f"/{resource}/{{{id_name}}}/{action}", transition, methods=["POST"],
|
||||
status_code=201 if action == "rollbacks" else 200,
|
||||
operation_id=f"{action}_{resource}")
|
||||
router.add_api_route(
|
||||
f"/{resource}/{{{id_name}}}/{action}",
|
||||
transition,
|
||||
methods=["POST"],
|
||||
status_code=201 if action == "rollbacks" else 200,
|
||||
operation_id=f"{action}_{resource}",
|
||||
)
|
||||
|
||||
|
||||
register_resource("config-releases", ReleasePayload, "release_id", update=False)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Analysis-only dynamic asset allocation endpoint."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.asset_allocation import AssetAllocationQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.asset_allocation_service import AssetAllocationService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-asset-allocation"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/asset-allocation")
|
||||
async def generate_asset_allocation(
|
||||
payload: AssetAllocationQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await AssetAllocationService(enforce_profile_governance=True).generate_for_agent(
|
||||
payload, context
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Investment-goal collection and goal-book review endpoints."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, status
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.investment_goals import (
|
||||
InvestmentGoalBookPublish,
|
||||
InvestmentGoalBookReview,
|
||||
InvestmentGoalConfirmation,
|
||||
InvestmentGoalCreate,
|
||||
)
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.investment_goal_service import InvestmentGoalService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-investment-goals"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/investment-goals", status_code=status.HTTP_201_CREATED)
|
||||
async def create_investment_goal(
|
||||
payload: InvestmentGoalCreate,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().create(payload, context, key)
|
||||
|
||||
|
||||
@router.get("/investment-goals/current")
|
||||
async def current_own_investment_goal(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().current(int(context.user_id), context)
|
||||
|
||||
|
||||
@router.get("/customers/{customer_id}/investment-goals/current")
|
||||
async def current_customer_investment_goal(
|
||||
customer_id: int,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().current(customer_id, context)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/confirmations")
|
||||
async def confirm_investment_goal(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalConfirmation,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().confirm(goal_no, context, key)
|
||||
|
||||
|
||||
@router.get("/investment-goals/{goal_no}/goal-book")
|
||||
async def investment_goal_book(
|
||||
goal_no: str,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, Any]:
|
||||
return await InvestmentGoalService().goal_book(goal_no, context)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/goal-book/reviews")
|
||||
async def review_investment_goal_book(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalBookReview,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().review_book(goal_no, payload, context, key)
|
||||
|
||||
|
||||
@router.post("/investment-goals/{goal_no}/goal-book/publications")
|
||||
async def publish_investment_goal_book(
|
||||
goal_no: str,
|
||||
payload: InvestmentGoalBookPublish,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await InvestmentGoalService().publish_book(goal_no, payload, context, key)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Mandatory customer onboarding endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, status
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.schemas.risk_questionnaire import RiskQuestionnaireSubmission
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.risk_questionnaire_service import RiskQuestionnaireService
|
||||
|
||||
router = APIRouter(prefix="/api/v1/onboarding", tags=["customer-onboarding"])
|
||||
|
||||
|
||||
@router.get("/risk-questionnaire")
|
||||
async def get_risk_questionnaire(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await RiskQuestionnaireService().questionnaire(context)
|
||||
|
||||
|
||||
@router.post("/risk-questionnaire/submissions", status_code=status.HTTP_201_CREATED)
|
||||
async def submit_risk_questionnaire(
|
||||
payload: RiskQuestionnaireSubmission,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await RiskQuestionnaireService().submit(payload, context, key)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Read-only portfolio-analysis endpoint."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.api.schemas.portfolio_analysis import PortfolioAnalysisQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.portfolio_analysis_service import PortfolioAnalysisService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/advisor", tags=["advisor-portfolio-analysis"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/portfolio-analysis")
|
||||
async def analyze_portfolio(
|
||||
payload: PortfolioAnalysisQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await PortfolioAnalysisService(enforce_profile_governance=True).analyze_for_agent(
|
||||
payload, context
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Recommendation generation and reviewed publication endpoints."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, Path
|
||||
|
||||
from app.api.dependencies.auth import build_request_context
|
||||
from app.api.dependencies.rate_limit import enforce_rate_limit
|
||||
from app.core.contracts import RequestContext
|
||||
from app.core.product_recommendation_contracts import ProductRecommendationQuery
|
||||
from app.service.advisor_rollout_service import enforce_advisor_rollout
|
||||
from app.service.product_recommendation_service import ProductRecommendationService
|
||||
|
||||
advisor_router = APIRouter(
|
||||
prefix="/api/v1/advisor",
|
||||
tags=["advisor-recommendations"],
|
||||
dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
admin_router = APIRouter(
|
||||
prefix="/api/v1/admin",
|
||||
tags=["platform-admin"],
|
||||
dependencies=[Depends(enforce_rate_limit)],
|
||||
)
|
||||
|
||||
|
||||
@advisor_router.post("/recommendations")
|
||||
async def generate_recommendation(
|
||||
payload: ProductRecommendationQuery,
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await ProductRecommendationService(enforce_profile_governance=True).generate(
|
||||
payload, context, key
|
||||
)
|
||||
|
||||
|
||||
@advisor_router.get("/recommendations/published")
|
||||
async def published_recommendations(
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
return await ProductRecommendationService().published(context)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/advisor/recommendations/{content_id}/reviews",
|
||||
dependencies=[Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
async def review_recommendation(
|
||||
payload: dict[str, Any],
|
||||
content_id: int = Path(gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
decision = payload.get("decision")
|
||||
if decision not in {"approved", "rejected"}:
|
||||
from app.core.errors import ValidationAgentError
|
||||
|
||||
raise ValidationAgentError("decision 必须为 approved 或 rejected")
|
||||
comment = payload.get("comment", "")
|
||||
if not isinstance(comment, str):
|
||||
raise ValueError("comment must be a string")
|
||||
return await ProductRecommendationService().review(content_id, decision, comment, context, key)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/advisor/recommendations/{content_id}/publications",
|
||||
dependencies=[Depends(enforce_advisor_rollout)],
|
||||
)
|
||||
async def publish_recommendation(
|
||||
content_id: int = Path(gt=0),
|
||||
context: RequestContext = Depends(build_request_context), # noqa: B008
|
||||
key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
) -> dict[str, object]:
|
||||
return await ProductRecommendationService().publish(content_id, context, key)
|
||||
Reference in New Issue
Block a user