AL-06: 对话 Tool 与 API 响应对齐 main 基准——chat_tools.suitability_check 返回体扩展 match_result/mismatch_type/requires_disclosure/needs_branch_confirm/block_response_code/rule_refs(替代 rule_id/reasons/effective_level 展示),check_source='r02_chat',not_found 转结构化 found=False(NotFoundError except 死代码清除);tool_service.summarize suitability 分支按新枚举重写文案(网点当面确认 FM-01/签署风险揭示书/测评过期 FM-03/专业豁免,附 rule_refs 依据);risk_api 断言改 block_response_code。全量 373 passed 0 failed(1 skip=test_suitability 占位待 AL-07)。遗留:chat 链路 risk_suitability_log.actor_id 暂落 SYSTEM(run_tool 统一透传需动 core_tools/kb_tools 冻结 func 契约,发起者留痕已由 agent_tool_call 覆盖)
This commit is contained in:
@@ -29,7 +29,6 @@ from app.repository.core_ro import CoreReadOnlyRepository
|
||||
from app.repository.risk_repository import RiskRepository
|
||||
from app.service import suitability
|
||||
from app.tool.core_tools import _jsonable
|
||||
from app.utils.exceptions import NotFoundError
|
||||
|
||||
|
||||
class RiskToolSpec(dict):
|
||||
@@ -132,34 +131,40 @@ def customer_context(customer_id: str, core_ro: CoreReadOnlyRepository | None =
|
||||
|
||||
def suitability_check(customer_id: str, core_ro: CoreReadOnlyRepository | None = None,
|
||||
risk_repo: RiskRepository | None = None, **params: Any) -> dict[str, Any]:
|
||||
"""适当性校验(SUIT-001~008):给定产品判断客户是否可购(只读核查,落审计日志)。
|
||||
"""适当性校验(main 矩阵契约):给定产品判断客户是否可购(只读核查,落审计日志)。
|
||||
|
||||
product_id 由 LLM/调用方经白名单传入;复用 suitability.suitability_check
|
||||
走原审计落库(risk_suitability_log 仅 INSERT)。NotFound(客户/产品缺失)
|
||||
转为结构化 found=False,不抛异常污染对话链路(run_tool 仍记 success 落库)。
|
||||
走原审计落库(risk_suitability_log 仅 INSERT,check_source='r02_chat')。
|
||||
AL-06 对齐 main 契约:返回体为 match_result 五值 / mismatch_type 七值 /
|
||||
requires_disclosure / needs_branch_confirm / block_response_code / rule_refs;
|
||||
客户或产品缺失为 not_found 结构(不再抛异常),对外仍表达为 found=False。
|
||||
"""
|
||||
product_id = params.get("product_id")
|
||||
if not product_id:
|
||||
return {"found": False, "error": "missing_product_id"}
|
||||
ro = core_ro or CoreReadOnlyRepository()
|
||||
repo = risk_repo or RiskRepository()
|
||||
try:
|
||||
result = suitability.suitability_check(customer_id, product_id, core_ro=ro, risk_repo=repo)
|
||||
except NotFoundError as exc:
|
||||
return {"found": False, "error": str(exc)}
|
||||
result = suitability.suitability_check(
|
||||
customer_id, product_id, core_ro=ro, risk_repo=repo, check_source="r02_chat"
|
||||
)
|
||||
if result.mismatch_type == "not_found":
|
||||
return {"found": False, "error": "customer or product not found"}
|
||||
return _jsonable(
|
||||
{
|
||||
"found": True,
|
||||
"customer_id": customer_id,
|
||||
"product_id": product_id,
|
||||
"customer_risk_level": result.customer_level,
|
||||
"effective_level": result.effective_level,
|
||||
"product_risk_level": result.product_level,
|
||||
"match_result": result.match_result,
|
||||
"mismatch_type": result.mismatch_type,
|
||||
"is_matched": result.is_matched,
|
||||
"blocked": result.blocked,
|
||||
"rule_id": result.rule_id,
|
||||
"requires_disclosure": result.requires_disclosure,
|
||||
"needs_branch_confirm": result.needs_branch_confirm,
|
||||
"block_response_code": result.block_response_code,
|
||||
"rule_refs": result.rule_refs,
|
||||
"block_reason": result.block_reason,
|
||||
"reasons": result.reasons,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -389,10 +389,24 @@ def summarize(record: dict[str, Any]) -> str:
|
||||
if name == "suitability_check":
|
||||
if not data.get("found"):
|
||||
return f"(适当性校验:{data.get('error') or '未找到客户或产品'})"
|
||||
verdict = "可购" if (data.get("is_matched") and not data.get("blocked")) else "不可购"
|
||||
# AL-06 对齐 main 契约:按 match_result/mismatch_type 新枚举重写文案
|
||||
if data.get("needs_branch_confirm"):
|
||||
verdict = "不可直接购买,需网点当面确认(FM-01)"
|
||||
elif data.get("match_result") == "professional_exempt":
|
||||
verdict = "可购(专业投资者豁免适当性匹配)"
|
||||
elif data.get("match_result") == "risk_expired":
|
||||
verdict = "不可购,风险测评已过期(FM-03),须重新测评"
|
||||
elif data.get("match_result") == "allowed_with_disclosure" and not data.get("blocked"):
|
||||
verdict = "可购,需签署风险揭示书"
|
||||
elif data.get("match_result") == "allowed" and not data.get("blocked"):
|
||||
verdict = "可购"
|
||||
else:
|
||||
verdict = "不可购(客户风险等级与产品风险等级不匹配)"
|
||||
refs = data.get("rule_refs") or []
|
||||
ref_txt = f";依据 {'、'.join(refs)}" if refs else ""
|
||||
return (
|
||||
f"(适当性校验:客户 {data.get('customer_risk_level')} 对产品 "
|
||||
f"{data.get('product_risk_level')} {verdict}({data.get('rule_id')});"
|
||||
f"{data.get('product_risk_level')} {verdict}{ref_txt};"
|
||||
f"仅供参考,最终以持证投顾审核为准)"
|
||||
)
|
||||
if name == "aml_lookup":
|
||||
|
||||
@@ -235,7 +235,8 @@ def test_suitability_check_by_owner_officer_and_assigned_advisor(client, env):
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["blocked"] is True and body["rule_id"] == "SUIT-001" # C1+R4
|
||||
# AL-06 main 契约:机器码断言改 block_response_code(SUIT-001 退役)
|
||||
assert body["blocked"] is True and body["block_response_code"] == "SUIT_RISK_MISMATCH" # C1+R4
|
||||
r = client.post(
|
||||
"/api/risk/suitability/check",
|
||||
json={"customer_id": "CUST-3001", "product_id": "PROD-510300"},
|
||||
|
||||
@@ -207,7 +207,8 @@ def test_suitability_check_matched(risk_env):
|
||||
d = record["data"]
|
||||
assert d["found"] is True
|
||||
assert d["is_matched"] is True and d["blocked"] is False
|
||||
assert d["rule_id"] == "SUIT-PASS"
|
||||
assert d["match_result"] == "allowed" # AL-06 main 契约:C1×R1 矩阵 allowed
|
||||
assert d["block_response_code"] == "SUIT_OK"
|
||||
|
||||
|
||||
def test_suitability_check_blocked_not_matched(risk_env):
|
||||
@@ -218,7 +219,9 @@ def test_suitability_check_blocked_not_matched(risk_env):
|
||||
assert record["status"] == "success"
|
||||
d = record["data"]
|
||||
assert d["is_matched"] is False and d["blocked"] is True
|
||||
assert d["rule_id"] == "SUIT-001"
|
||||
assert d["match_result"] == "forbidden" and d["mismatch_type"] == "risk_level"
|
||||
assert d["block_response_code"] == "SUIT_RISK_MISMATCH"
|
||||
assert d["rule_refs"] == ["JR-AST-012"]
|
||||
|
||||
|
||||
def test_suitability_check_product_not_found(risk_env):
|
||||
|
||||
Reference in New Issue
Block a user