feat: 完成三角色工作台及金租对公Skills全流程演示
This commit is contained in:
@@ -17,9 +17,11 @@ from pydantic import BaseModel, Field
|
||||
from api.risk_agent_access import authorize, check_project, visible_result
|
||||
from api.risk_agent_alignment import align
|
||||
from api.risk_agent_connectors import execute
|
||||
from services.business_demo_database import DEFAULT_PATH as BUSINESS_DB_PATH, build_business_demo_database
|
||||
|
||||
router = APIRouter(prefix="/api/xingbang/risk-agent", tags=["风险研判 Agent"], dependencies=[Depends(authorize)])
|
||||
DB_PATH = Path(__file__).resolve().parents[1] / "xingbang_data" / "risk_agent.sqlite3"
|
||||
SKILLS_BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
SEED = {
|
||||
"connections": [{"id":"conn-erp","name":"核心 ERP 只读库","type":"PostgreSQL","host":"10.20.8.16","database":"xb_erp","status":"healthy","latency":38,"updated":"今天 09:42"}],
|
||||
@@ -74,19 +76,192 @@ for plan in SEED["plans"]:
|
||||
plan.update(runs=0, status="published")
|
||||
plan["skills"] = [x for x in plan["skills"] if x != "skill-trend"]
|
||||
|
||||
BUSINESS_TABLES = [
|
||||
"corporate_customer", "lease_project", "financing_scheme", "financial_statement",
|
||||
"lease_asset", "insurance_policy", "guarantee_measure", "credit_snapshot",
|
||||
"legal_case", "repayment_source", "project_document",
|
||||
]
|
||||
SEED["connections"].append({
|
||||
"id": "conn-business-demo", "name": "对公租赁多表业务库(演示)", "type": "SQLite",
|
||||
"database": str(BUSINESS_DB_PATH), "data_domain": "demo", "allowed_tables": BUSINESS_TABLES,
|
||||
"timeout": 10, "max_rows": 500, "status": "published", "latency": None,
|
||||
"updated": "随演示库自动生成",
|
||||
})
|
||||
SEED["cards"].extend([
|
||||
{
|
||||
"id": "card-business-project", "name": "项目与授信方案", "category": "项目",
|
||||
"description": "从客户、项目和当前融资方案三张表读取项目基础事实",
|
||||
"connection": "conn-business-demo", "tables": ["corporate_customer", "lease_project", "financing_scheme"],
|
||||
"parameter": "project_id", "sql": """SELECT p.project_id, c.customer_name, f.facility_amount, f.facility_term_months
|
||||
FROM lease_project p
|
||||
JOIN corporate_customer c ON c.customer_id = p.customer_id
|
||||
JOIN financing_scheme f ON f.project_id = p.project_id AND f.status = '生效'
|
||||
WHERE p.project_id = :project_id""",
|
||||
"grain": "每个项目一条当前生效方案", "mapping": {
|
||||
"project.id": "project_id", "customer.name": "customer_name",
|
||||
"facility.amount": "facility_amount", "facility.term": "facility_term_months",
|
||||
},
|
||||
"field_units": {"facility.amount": "万元"},
|
||||
"alignment": {"project_field": "project_id", "selection": "single"},
|
||||
"skills": ["skill-quality"], "status": "published", "version": "v1.0", "coverage": None, "lastRun": "尚未运行",
|
||||
},
|
||||
{
|
||||
"id": "card-business-finance", "name": "多期财务报表", "category": "财务",
|
||||
"description": "按项目读取多期合并报表,由数据卡选择最新一期并保留历史记录",
|
||||
"connection": "conn-business-demo", "tables": ["financial_statement"], "parameter": "project_id",
|
||||
"sql": """SELECT project_id, period, scope, currency_unit, total_assets, total_liabilities, operating_cashflow
|
||||
FROM financial_statement
|
||||
WHERE project_id = :project_id
|
||||
ORDER BY period""",
|
||||
"grain": "项目 + 期间 + 口径", "mapping": {
|
||||
"financial.period": "period", "financial.scope": "scope",
|
||||
"financial.total_assets": "total_assets", "financial.total_liabilities": "total_liabilities",
|
||||
"financial.operating_cashflow": "operating_cashflow",
|
||||
},
|
||||
"field_units": {"financial.total_assets": "万元", "financial.total_liabilities": "万元", "financial.operating_cashflow": "万元"},
|
||||
"alignment": {"project_field": "project_id", "period_field": "period", "scope_field": "scope", "selection": "latest", "target_scope": "合并"},
|
||||
"skills": ["skill-quality", "skill-finance"], "status": "published", "version": "v1.0", "coverage": None, "lastRun": "尚未运行",
|
||||
},
|
||||
{
|
||||
"id": "card-business-credit", "name": "征信与涉诉汇总", "category": "征信",
|
||||
"description": "读取最新征信快照,并汇总项目涉诉数量和金额",
|
||||
"connection": "conn-business-demo", "tables": ["credit_snapshot", "legal_case"], "parameter": "project_id",
|
||||
"sql": """SELECT c.project_id, c.report_date, c.overdue_count, c.query_count_6m,
|
||||
COUNT(l.case_id) AS legal_case_count, COALESCE(SUM(l.amount), 0) AS legal_amount
|
||||
FROM credit_snapshot c
|
||||
LEFT JOIN legal_case l ON l.project_id = c.project_id
|
||||
WHERE c.project_id = :project_id
|
||||
AND c.report_date = (SELECT MAX(c2.report_date) FROM credit_snapshot c2 WHERE c2.project_id = :project_id)
|
||||
GROUP BY c.project_id, c.report_date, c.overdue_count, c.query_count_6m""",
|
||||
"grain": "项目 + 征信报告日期", "mapping": {
|
||||
"credit.report_date": "report_date", "credit.overdue_count": "overdue_count",
|
||||
"credit.query_count_6m": "query_count_6m", "legal.case_count": "legal_case_count", "legal.amount": "legal_amount",
|
||||
},
|
||||
"field_units": {"legal.amount": "万元"},
|
||||
"period": "最新征信", "alignment": {"project_field": "project_id", "selection": "single"},
|
||||
"skills": ["skill-quality", "skill-credit"], "status": "published", "version": "v1.0", "coverage": None, "lastRun": "尚未运行",
|
||||
},
|
||||
{
|
||||
"id": "card-business-lease", "name": "租赁物与增信汇总", "category": "增信",
|
||||
"description": "按项目汇总租赁物价值、重复编号、保险、保证和保证金",
|
||||
"connection": "conn-business-demo", "tables": ["lease_project", "lease_asset", "insurance_policy", "guarantee_measure"],
|
||||
"parameter": "project_id", "sql": """WITH asset_summary AS (
|
||||
SELECT project_id, SUM(appraised_value) AS lease_total_value,
|
||||
COUNT(*) - COUNT(DISTINCT serial_no) AS duplicate_asset_count
|
||||
FROM lease_asset GROUP BY project_id
|
||||
), policy_summary AS (
|
||||
SELECT project_id, SUM(insured_amount) AS insurance_amount
|
||||
FROM insurance_policy WHERE status = '有效' GROUP BY project_id
|
||||
), guarantee_summary AS (
|
||||
SELECT project_id,
|
||||
SUM(CASE WHEN guarantee_type = '保证金' THEN guarantee_amount ELSE 0 END) AS deposit_amount,
|
||||
SUM(CASE WHEN guarantee_type <> '保证金' THEN guarantee_amount ELSE 0 END) AS guarantee_amount
|
||||
FROM guarantee_measure GROUP BY project_id
|
||||
)
|
||||
SELECT p.project_id, a.lease_total_value, a.duplicate_asset_count,
|
||||
COALESCE(i.insurance_amount, 0) AS insurance_amount,
|
||||
COALESCE(g.deposit_amount, 0) AS deposit_amount,
|
||||
COALESCE(g.guarantee_amount, 0) AS guarantee_amount
|
||||
FROM lease_project p
|
||||
LEFT JOIN asset_summary a ON a.project_id = p.project_id
|
||||
LEFT JOIN policy_summary i ON i.project_id = p.project_id
|
||||
LEFT JOIN guarantee_summary g ON g.project_id = p.project_id
|
||||
WHERE p.project_id = :project_id""",
|
||||
"grain": "每个项目一条增信汇总", "mapping": {
|
||||
"lease.total_value": "lease_total_value", "lease.duplicate_count": "duplicate_asset_count",
|
||||
"insurance.amount": "insurance_amount", "deposit.amount": "deposit_amount", "guarantee.amount": "guarantee_amount",
|
||||
},
|
||||
"field_units": {"lease.total_value": "万元", "insurance.amount": "万元", "deposit.amount": "万元", "guarantee.amount": "万元"},
|
||||
"alignment": {"project_field": "project_id", "selection": "single"},
|
||||
"skills": ["skill-quality", "skill-coverage"], "status": "published", "version": "v1.0", "coverage": None, "lastRun": "尚未运行",
|
||||
},
|
||||
{
|
||||
"id": "card-business-repayment", "name": "还款来源与材料", "category": "还款",
|
||||
"description": "读取项目还款来源、预计金额和佐证材料状态",
|
||||
"connection": "conn-business-demo", "tables": ["repayment_source"], "parameter": "project_id",
|
||||
"sql": """SELECT project_id, source_type, annual_amount, evidence_status,
|
||||
CASE WHEN source_type = '特定项目回款' THEN 'true' ELSE 'false' END AS collection_dependency
|
||||
FROM repayment_source
|
||||
WHERE project_id = :project_id""",
|
||||
"grain": "项目 + 还款来源", "mapping": {
|
||||
"collection.dependency": "collection_dependency", "collection.evidence": "evidence_status",
|
||||
"repayment.annual_amount": "annual_amount", "repayment.source_type": "source_type",
|
||||
},
|
||||
"field_units": {"repayment.annual_amount": "万元"},
|
||||
"alignment": {"project_field": "project_id", "selection": "single"},
|
||||
"skills": ["skill-quality"], "status": "published", "version": "v1.0", "coverage": None, "lastRun": "尚未运行",
|
||||
},
|
||||
])
|
||||
SEED["plans"].append({
|
||||
"id": "plan-business-sql", "name": "对公多表业务库预审", "description": "通过受控 SQL 从多张业务表读取项目、财务、征信、租赁物和还款来源",
|
||||
"cards": ["card-business-project", "card-business-finance", "card-business-credit", "card-business-lease", "card-business-repayment"],
|
||||
"skills": ["skill-quality", "skill-finance", "skill-credit", "skill-coverage"], "rules": ["rules-corporate"],
|
||||
"status": "published", "version": "v1.0", "runs": 0,
|
||||
})
|
||||
|
||||
FINANCIAL_LEASING_SKILLS = [
|
||||
{
|
||||
"id": "skill-fl-lessee", "name": "承租人及还款能力核查", "category": "承租人尽调",
|
||||
"description": "核查经营、财务、信用、还款来源及尽调证据是否足以支持送审",
|
||||
"engine": "model", "mode": "mini-openclaw + 本地模型", "inputs": [
|
||||
"customer.name", "financial.total_assets", "financial.total_liabilities",
|
||||
"financial.operating_cashflow", "repayment.source_type", "repayment.annual_amount",
|
||||
"collection.evidence", "credit.report_date", "credit.overdue_count", "legal.case_count", "legal.amount",
|
||||
], "output": "advisory.lessee", "instructions": "按承租人尽调方法核查第一还款来源、经营财务、征信涉诉和资料真实性,逐项引用证据。",
|
||||
"native_skills": ["financial-leasing-lessee-review/SKILL.md"], "status": "published", "version": "v1.0",
|
||||
},
|
||||
{
|
||||
"id": "skill-fl-asset", "name": "租赁物适格性与权属核查", "category": "租赁物核验",
|
||||
"description": "核查租赁物真实性、权属、特定化、价值、保险和可处置性",
|
||||
"engine": "model", "mode": "mini-openclaw + 本地模型", "inputs": ["lease.total_value", "lease.duplicate_count", "insurance.amount", "facility.amount"],
|
||||
"output": "advisory.lease_asset", "instructions": "逐项核对租赁物适格性、权属、设备标识、估值与保险证据,不作权属或审批结论。",
|
||||
"native_skills": ["financial-leasing-asset-review/SKILL.md"], "status": "published", "version": "v1.0",
|
||||
},
|
||||
{
|
||||
"id": "skill-fl-sale-leaseback", "name": "售后回租真实性核查", "category": "售后回租",
|
||||
"description": "核查原始权属、所有权转移、估值、融资需求和资金用途真实性",
|
||||
"engine": "model", "mode": "mini-openclaw + 本地模型", "inputs": ["facility.amount", "lease.total_value", "repayment.source_type", "collection.evidence"],
|
||||
"output": "advisory.sale_leaseback", "instructions": "先判断是否适用售后回租,再核查原始权属、转移、估值、融资需求和资金用途证据。",
|
||||
"native_skills": ["financial-leasing-sale-leaseback-review/SKILL.md"], "status": "published", "version": "v1.0",
|
||||
},
|
||||
{
|
||||
"id": "skill-fl-guarantee", "name": "增信、关联交易与集中度核查", "category": "增信与集中度",
|
||||
"description": "核查保证与抵质押有效性,识别关联关系和集中度数据缺口",
|
||||
"engine": "model", "mode": "mini-openclaw + 本地模型", "inputs": ["guarantee.amount", "deposit.amount", "insurance.amount", "legal.case_count", "credit.overdue_count"],
|
||||
"output": "advisory.guarantee", "instructions": "分开评价各类增信,穿透关联方,并明确集中度判断所需的组合数据。",
|
||||
"native_skills": ["financial-leasing-guarantee-concentration/SKILL.md"], "status": "published", "version": "v1.0",
|
||||
},
|
||||
{
|
||||
"id": "skill-fl-review-report", "name": "独立评审与报告证据核查", "category": "评审报告",
|
||||
"description": "将数据来源、AI判断、评审员意见和最终报告逐项建立证据对应",
|
||||
"engine": "model", "mode": "mini-openclaw + 本地模型", "inputs": [], "output": "advisory.review_report",
|
||||
"instructions": "全链路独立复核并形成可追溯报告候选,不覆盖人工确认内容。",
|
||||
"native_skills": ["financial-leasing-review-report/SKILL.md"], "status": "published", "version": "v1.0",
|
||||
},
|
||||
]
|
||||
SEED["skills"].extend(FINANCIAL_LEASING_SKILLS)
|
||||
SEED["plans"].append({
|
||||
"id": "plan-financial-leasing-corporate", "name": "金租对公项目全链路预审",
|
||||
"description": "按现行金租监管框架核查承租人、租赁物、售后回租、增信与集中度",
|
||||
"cards": ["card-business-project", "card-business-finance", "card-business-credit", "card-business-lease", "card-business-repayment"],
|
||||
"skills": ["skill-quality", "skill-finance", "skill-credit", "skill-coverage", "skill-fl-lessee", "skill-fl-asset", "skill-fl-sale-leaseback", "skill-fl-guarantee"],
|
||||
"rules": ["rules-corporate"], "status": "published", "version": "v1.0", "runs": 0,
|
||||
})
|
||||
|
||||
def _now(): return datetime.now(timezone.utc).isoformat()
|
||||
@contextmanager
|
||||
def _db():
|
||||
build_business_demo_database()
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
db=sqlite3.connect(DB_PATH); db.execute("CREATE TABLE IF NOT EXISTS catalog (kind TEXT, id TEXT, body TEXT, PRIMARY KEY(kind,id))")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, body TEXT)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS revisions (kind TEXT, id TEXT, version TEXT, body TEXT)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS reviews (id TEXT PRIMARY KEY, kind TEXT, item_id TEXT, body TEXT)")
|
||||
db.execute("CREATE TABLE IF NOT EXISTS audit (time TEXT, actor TEXT, action TEXT, detail TEXT)")
|
||||
if not db.execute("SELECT 1 FROM catalog LIMIT 1").fetchone():
|
||||
for kind, rows in SEED.items():
|
||||
for row in rows: db.execute("INSERT INTO catalog VALUES (?,?,?)",(kind,row["id"],json.dumps(row,ensure_ascii=False)))
|
||||
db.commit()
|
||||
for kind, rows in SEED.items():
|
||||
for row in rows:
|
||||
db.execute("INSERT OR IGNORE INTO catalog VALUES (?,?,?)",(kind,row["id"],json.dumps(row,ensure_ascii=False)))
|
||||
db.commit()
|
||||
try:
|
||||
with db:
|
||||
yield db
|
||||
@@ -127,7 +302,7 @@ def validate(kind, body):
|
||||
raise HTTPException(422,"原生 Skills 必须为勾选列表")
|
||||
if native:
|
||||
from tools.skills_scanner import discover_skills
|
||||
available={s["id"] for s in discover_skills(Path(__file__).resolve().parents[1])}
|
||||
available={s["id"] for s in discover_skills(SKILLS_BASE_DIR)}
|
||||
if not set(native).issubset(available): raise HTTPException(422,"所选原生 Skill 已不存在")
|
||||
if kind == "skills" and body.get("engine")=="ratio":
|
||||
if not all(isinstance(body.get(k),str) and body[k].strip() for k in ("numerator","denominator","output")):
|
||||
@@ -172,7 +347,58 @@ def context(request:Request):
|
||||
@router.get("/native-skills/list")
|
||||
def native_skills():
|
||||
from tools.skills_scanner import discover_skills
|
||||
return [{k:s[k] for k in ("id","name","description")} for s in discover_skills(Path(__file__).resolve().parents[1])]
|
||||
return [{k:s[k] for k in ("id","name","description")} for s in discover_skills(SKILLS_BASE_DIR)]
|
||||
|
||||
class NativeSkillUpload(BaseModel):
|
||||
skill_md: str = Field(min_length=1, max_length=65536)
|
||||
|
||||
class NativeSkillImport(BaseModel):
|
||||
skills: list[NativeSkillUpload] = Field(min_length=1, max_length=20)
|
||||
|
||||
@router.post("/native-skills/import", status_code=201)
|
||||
def import_native_skills(payload: NativeSkillImport, request: Request):
|
||||
if request.state.identity["role"] != "admin":
|
||||
raise HTTPException(403, "仅配置管理员可以导入本地 Skill")
|
||||
from tools.skills_scanner import install_skill, parse_skill, skill_slug
|
||||
prepared = []
|
||||
for uploaded in payload.skills:
|
||||
try:
|
||||
meta = parse_skill(uploaded.skill_md)
|
||||
prepared.append((uploaded.skill_md, meta, skill_slug(meta)))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc)) from exc
|
||||
if len({slug for _, _, slug in prepared}) != len(prepared):
|
||||
raise HTTPException(409, "所选文件夹中存在同名 Skill")
|
||||
imported = []
|
||||
with _db() as db:
|
||||
existing = {item["id"] for item in _all(db, "skills")}
|
||||
skill_root = SKILLS_BASE_DIR / "skills" / "imported"
|
||||
for _, _, slug in prepared:
|
||||
if (skill_root / slug / "SKILL.md").exists() or "skill-native-" + slug in existing:
|
||||
raise HTTPException(409, f"同名 Skill 已存在:{slug}")
|
||||
for content, meta, slug in prepared:
|
||||
try:
|
||||
installed = install_skill(SKILLS_BASE_DIR, content)
|
||||
except FileExistsError as exc:
|
||||
raise HTTPException(409, str(exc)) from exc
|
||||
except (OSError, ValueError) as exc:
|
||||
raise HTTPException(422, str(exc)) from exc
|
||||
item_id = "skill-native-" + slug
|
||||
display_name = meta.get("display_name", meta["name"])
|
||||
body = {
|
||||
"id": item_id, "name": str(display_name)[:120], "category": str(meta.get("category", "本地导入"))[:40],
|
||||
"description": meta["description"], "engine": "model", "mode": "mini-openclaw + 本地模型",
|
||||
"inputs": meta.get("inputs", []), "output": str(meta.get("output", "advisory." + slug.replace("-", "_"))),
|
||||
"instructions": str(meta.get("instructions", meta["description"])), "native_skills": [installed["legacy_id"]],
|
||||
"status": "published" if request.state.identity["preview"] else "draft",
|
||||
"version": str(meta.get("version", "v1")), "source": "local-folder", "updatedAt": _now(),
|
||||
"edited_by": request.state.identity["id"],
|
||||
}
|
||||
persist(db, "skills", body)
|
||||
audit(db, request, "native_skill_import", {"id": item_id, "native_skill": installed["legacy_id"], "status": body["status"]})
|
||||
existing.add(item_id)
|
||||
imported.append(body)
|
||||
return {"count": len(imported), "skills": imported}
|
||||
|
||||
def audit(db, request, action, detail):
|
||||
db.execute("INSERT INTO audit VALUES (?,?,?,?)",(_now(),request.state.identity["id"],action,json.dumps(detail,ensure_ascii=False)))
|
||||
|
||||
Reference in New Issue
Block a user