deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
c2f683690e
commit
b8a727b375
@ -1,160 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_agent_def",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "Agent编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"agent_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"name": {
|
||||
"label": "名称",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"model_route": {
|
||||
"label": "pipeline-llm 路由",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"system_prompt_txt": {
|
||||
"label": "提示词",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"tool_whitelist_json": {
|
||||
"label": "工具白名单",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"permission_mode": {
|
||||
"label": "read/write 模式",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"is_enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "Agent编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"agent_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"name": {
|
||||
"label": "名称",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"model_route": {
|
||||
"label": "pipeline-llm 路由",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"system_prompt_txt": {
|
||||
"label": "提示词",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"tool_whitelist_json": {
|
||||
"label": "工具白名单",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"permission_mode": {
|
||||
"label": "read/write 模式",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"is_enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"code": {
|
||||
"label": "Agent编码",
|
||||
"type": "select"
|
||||
},
|
||||
"agent_type": {
|
||||
"label": "类型",
|
||||
"type": "select"
|
||||
},
|
||||
"name": {
|
||||
"label": "名称",
|
||||
"type": "select"
|
||||
},
|
||||
"model_route": {
|
||||
"label": "pipeline-llm 路由",
|
||||
"type": "select"
|
||||
},
|
||||
"system_prompt_txt": {
|
||||
"label": "提示词",
|
||||
"type": "text"
|
||||
},
|
||||
"tool_whitelist_json": {
|
||||
"label": "工具白名单",
|
||||
"type": "text"
|
||||
},
|
||||
"permission_mode": {
|
||||
"label": "read/write 模式",
|
||||
"type": "select"
|
||||
},
|
||||
"is_enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox"
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_agent_tool_call",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"trace_id": {
|
||||
"label": "轨迹ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"request_json": {
|
||||
"label": "请求",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"response_json": {
|
||||
"label": "响应",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"adjudication_code": {
|
||||
"label": "裁决结论",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"trace_id": {
|
||||
"label": "轨迹ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"request_json": {
|
||||
"label": "请求",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"response_json": {
|
||||
"label": "响应",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"adjudication_code": {
|
||||
"label": "裁决结论",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"trace_id": {
|
||||
"label": "轨迹ID",
|
||||
"type": "number"
|
||||
},
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select"
|
||||
},
|
||||
"request_json": {
|
||||
"label": "请求",
|
||||
"type": "text"
|
||||
},
|
||||
"response_json": {
|
||||
"label": "响应",
|
||||
"type": "text"
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox"
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select"
|
||||
},
|
||||
"adjudication_code": {
|
||||
"label": "裁决结论",
|
||||
"type": "select"
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_agent_trace",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"trace_uid": {
|
||||
"label": "轨迹UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"blueprint_id": {
|
||||
"label": "目标蓝图",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"input_ref_json": {
|
||||
"label": "要素1 输入引用",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"thought_txt": {
|
||||
"label": "要素2 思考",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"action_code": {
|
||||
"label": "要素3 动作",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"action_params_json": {
|
||||
"label": "要素4 动作参数",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"observation_json": {
|
||||
"label": "要素5 观察",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"result_state": {
|
||||
"label": "要素6 结果状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"llm_calls": {
|
||||
"label": "要素7 LLM调用数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发用户",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"trace_uid": {
|
||||
"label": "轨迹UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"blueprint_id": {
|
||||
"label": "目标蓝图",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"input_ref_json": {
|
||||
"label": "要素1 输入引用",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"thought_txt": {
|
||||
"label": "要素2 思考",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"action_code": {
|
||||
"label": "要素3 动作",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"action_params_json": {
|
||||
"label": "要素4 动作参数",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"observation_json": {
|
||||
"label": "要素5 观察",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"result_state": {
|
||||
"label": "要素6 结果状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"llm_calls": {
|
||||
"label": "要素7 LLM调用数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发用户",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"trace_uid": {
|
||||
"label": "轨迹UID",
|
||||
"type": "text"
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select"
|
||||
},
|
||||
"blueprint_id": {
|
||||
"label": "目标蓝图",
|
||||
"type": "number"
|
||||
},
|
||||
"input_ref_json": {
|
||||
"label": "要素1 输入引用",
|
||||
"type": "text"
|
||||
},
|
||||
"thought_txt": {
|
||||
"label": "要素2 思考",
|
||||
"type": "text"
|
||||
},
|
||||
"action_code": {
|
||||
"label": "要素3 动作",
|
||||
"type": "select"
|
||||
},
|
||||
"action_params_json": {
|
||||
"label": "要素4 动作参数",
|
||||
"type": "text"
|
||||
},
|
||||
"observation_json": {
|
||||
"label": "要素5 观察",
|
||||
"type": "text"
|
||||
},
|
||||
"result_state": {
|
||||
"label": "要素6 结果状态",
|
||||
"type": "select"
|
||||
},
|
||||
"llm_calls": {
|
||||
"label": "要素7 LLM调用数",
|
||||
"type": "number"
|
||||
},
|
||||
"duration_ms": {
|
||||
"label": "耗时",
|
||||
"type": "number"
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发用户",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_approval",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"approval_uid": {
|
||||
"label": "审批UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"approval_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"target_type": {
|
||||
"label": "对象类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"target_id": {
|
||||
"label": "对象ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "变更内容",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"requested_by": {
|
||||
"label": "发起人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"decided_by": {
|
||||
"label": "决策人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"decided_at": {
|
||||
"label": "决策时间",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"comment_txt": {
|
||||
"label": "意见",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"approval_uid": {
|
||||
"label": "审批UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"approval_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"target_type": {
|
||||
"label": "对象类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"target_id": {
|
||||
"label": "对象ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "变更内容",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"requested_by": {
|
||||
"label": "发起人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"decided_by": {
|
||||
"label": "决策人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"decided_at": {
|
||||
"label": "决策时间",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"comment_txt": {
|
||||
"label": "意见",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"approval_uid": {
|
||||
"label": "审批UID",
|
||||
"type": "text"
|
||||
},
|
||||
"approval_type": {
|
||||
"label": "类型",
|
||||
"type": "select"
|
||||
},
|
||||
"target_type": {
|
||||
"label": "对象类型",
|
||||
"type": "select"
|
||||
},
|
||||
"target_id": {
|
||||
"label": "对象ID",
|
||||
"type": "number"
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "变更内容",
|
||||
"type": "text"
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select"
|
||||
},
|
||||
"requested_by": {
|
||||
"label": "发起人",
|
||||
"type": "text"
|
||||
},
|
||||
"decided_by": {
|
||||
"label": "决策人",
|
||||
"type": "text"
|
||||
},
|
||||
"decided_at": {
|
||||
"label": "决策时间",
|
||||
"type": "text"
|
||||
},
|
||||
"comment_txt": {
|
||||
"label": "意见",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_llm_call_log",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"call_uid": {
|
||||
"label": "调用UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"route": {
|
||||
"label": "路由",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"model": {
|
||||
"label": "实际模型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"prompt_hash": {
|
||||
"label": "提示词哈希",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"completion_tokens": {
|
||||
"label": "tokens",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"fallback_used": {
|
||||
"label": "走兜底",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"call_uid": {
|
||||
"label": "调用UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"route": {
|
||||
"label": "路由",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"model": {
|
||||
"label": "实际模型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"prompt_hash": {
|
||||
"label": "提示词哈希",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"completion_tokens": {
|
||||
"label": "tokens",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"fallback_used": {
|
||||
"label": "走兜底",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"call_uid": {
|
||||
"label": "调用UID",
|
||||
"type": "text"
|
||||
},
|
||||
"agent_code": {
|
||||
"label": "Agent",
|
||||
"type": "select"
|
||||
},
|
||||
"route": {
|
||||
"label": "路由",
|
||||
"type": "select"
|
||||
},
|
||||
"model": {
|
||||
"label": "实际模型",
|
||||
"type": "select"
|
||||
},
|
||||
"prompt_hash": {
|
||||
"label": "提示词哈希",
|
||||
"type": "text"
|
||||
},
|
||||
"completion_tokens": {
|
||||
"label": "tokens",
|
||||
"type": "number"
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number"
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select"
|
||||
},
|
||||
"error_code": {
|
||||
"label": "错误码",
|
||||
"type": "select"
|
||||
},
|
||||
"fallback_used": {
|
||||
"label": "走兜底",
|
||||
"type": "checkbox"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
{
|
||||
"tblname": "pbl_tool_registry",
|
||||
"alias": "pbl_agent_runtime",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"agent_scope": {
|
||||
"label": "作用域",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"permission_required": {
|
||||
"label": "所需权限",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"params_schema_json": {
|
||||
"label": "参数白名单Schema",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"disabled_reason": {
|
||||
"label": "禁用原因",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"agent_scope": {
|
||||
"label": "作用域",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"permission_required": {
|
||||
"label": "所需权限",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"params_schema_json": {
|
||||
"label": "参数白名单Schema",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"disabled_reason": {
|
||||
"label": "禁用原因",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"tool_key": {
|
||||
"label": "工具键",
|
||||
"type": "select"
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select"
|
||||
},
|
||||
"agent_scope": {
|
||||
"label": "作用域",
|
||||
"type": "select"
|
||||
},
|
||||
"permission_required": {
|
||||
"label": "所需权限",
|
||||
"type": "select"
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox"
|
||||
},
|
||||
"params_schema_json": {
|
||||
"label": "参数白名单Schema",
|
||||
"type": "text"
|
||||
},
|
||||
"disabled_reason": {
|
||||
"label": "禁用原因",
|
||||
"type": "select"
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_agent_def",
|
||||
"comment": "Agent 定义(Designer/Critic)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"comment": "Agent编码",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "agent_type",
|
||||
"comment": "类型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"comment": "名称",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 128
|
||||
},
|
||||
{
|
||||
"name": "model_route",
|
||||
"comment": "pipeline-llm 路由",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "system_prompt_txt",
|
||||
"comment": "提示词",
|
||||
"null": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "tool_whitelist_json",
|
||||
"comment": "工具白名单",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "permission_mode",
|
||||
"comment": "read/write 模式",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "is_enabled",
|
||||
"comment": "启用",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "version_no",
|
||||
"comment": "版本",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_ad_code",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_agent_tool_call",
|
||||
"comment": "工具调用明细(服务端裁决留痕)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "trace_id",
|
||||
"comment": "轨迹ID",
|
||||
"null": false,
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"name": "tool_key",
|
||||
"comment": "工具键",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "request_json",
|
||||
"comment": "请求",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "response_json",
|
||||
"comment": "响应",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "ok",
|
||||
"comment": "成功",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "error_code",
|
||||
"comment": "错误码",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "adjudication_code",
|
||||
"comment": "裁决结论",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "duration_ms",
|
||||
"comment": "耗时",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_tc_trace",
|
||||
"unique": false,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"trace_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_agent_trace",
|
||||
"comment": "Agent 执行轨迹(第28章 7 要素)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "trace_uid",
|
||||
"comment": "轨迹UID",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "agent_code",
|
||||
"comment": "Agent",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "blueprint_id",
|
||||
"comment": "目标蓝图",
|
||||
"null": false,
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"name": "input_ref_json",
|
||||
"comment": "要素1 输入引用",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "thought_txt",
|
||||
"comment": "要素2 思考",
|
||||
"null": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "action_code",
|
||||
"comment": "要素3 动作",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "action_params_json",
|
||||
"comment": "要素4 动作参数",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "observation_json",
|
||||
"comment": "要素5 观察",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "result_state",
|
||||
"comment": "要素6 结果状态",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "llm_calls",
|
||||
"comment": "要素7 LLM调用数",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "duration_ms",
|
||||
"comment": "耗时",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"comment": "触发用户",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_at_uid",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"trace_uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_at_tenant",
|
||||
"unique": false,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"created_at"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,139 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_approval",
|
||||
"comment": "人工审批单(14.2 四类,无绕过路径)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "approval_uid",
|
||||
"comment": "审批UID",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "approval_type",
|
||||
"comment": "类型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "target_type",
|
||||
"comment": "对象类型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "target_id",
|
||||
"comment": "对象ID",
|
||||
"null": false,
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"name": "payload_json",
|
||||
"comment": "变更内容",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"comment": "状态",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "requested_by",
|
||||
"comment": "发起人",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "decided_by",
|
||||
"comment": "决策人",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "decided_at",
|
||||
"comment": "决策时间",
|
||||
"null": true,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "comment_txt",
|
||||
"comment": "意见",
|
||||
"null": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_ap_uid",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"approval_uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_ap_status",
|
||||
"unique": false,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "status",
|
||||
"appcode": "pbl_approval_status"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_llm_call_log",
|
||||
"comment": "LLM 调用日志(超时/重试/限流/兜底留痕)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "call_uid",
|
||||
"comment": "调用UID",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "agent_code",
|
||||
"comment": "Agent",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "route",
|
||||
"comment": "路由",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"comment": "实际模型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "prompt_hash",
|
||||
"comment": "提示词哈希",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "completion_tokens",
|
||||
"comment": "tokens",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "latency_ms",
|
||||
"comment": "时延",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"comment": "状态",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "error_code",
|
||||
"comment": "错误码",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "fallback_used",
|
||||
"comment": "走兜底",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_lc_uid",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"call_uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_tool_registry",
|
||||
"comment": "工具注册表(13 启用 / 9 禁用含 pbl.publish)",
|
||||
"module": "pbl_agent_runtime",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "tool_key",
|
||||
"comment": "工具键",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"comment": "标题",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 128
|
||||
},
|
||||
{
|
||||
"name": "agent_scope",
|
||||
"comment": "作用域",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "permission_required",
|
||||
"comment": "所需权限",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"comment": "启用",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "params_schema_json",
|
||||
"comment": "参数白名单Schema",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "disabled_reason",
|
||||
"comment": "禁用原因",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "version_no",
|
||||
"comment": "版本",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_tr_key",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"tool_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,9 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_agent_runtime —— Designer/Critic Agent 运行时与 fail-closed 工具裁决(M4a/M4b)。
|
||||
"""pbl_agent_runtime —— Designer/Critic Agent 运行时与 fail-closed 工具裁决(M4a)。
|
||||
|
||||
包出口:
|
||||
from pbl_agent_runtime.init import load_pbl_agent_runtime
|
||||
M4a 交付范围:
|
||||
* Agent 定义:designer(可写)/ critic(**零写权限**)
|
||||
* 工具注册表:13 启用 + 9 禁用(含 blueprint.publish_auto 等自动发布类禁用)
|
||||
* fail-closed 裁决:S1~S8 八步顺序,任一步不过即 DENY,默认裁决 = DENY
|
||||
* 四类强制人工审批:publish / compile_execute / blueprint_approve / tool_registry_change
|
||||
* 5 张表:pbl_agent_def / pbl_agent_tool / pbl_agent_trace /
|
||||
pbl_agent_trace_stage / pbl_approval
|
||||
|
||||
包出口(三处同步:定义 + 本文件导出 + init.py 注册):
|
||||
from pbl_agent_runtime import load_pbl_agent_runtime # 宿主应用标准入口
|
||||
from pbl_agent_runtime import load_pbl_agent_runtime_m4a # M4a 装配(建表+seed)
|
||||
from pbl_agent_runtime import load_m4a # 租户级门面 M4aApi
|
||||
"""
|
||||
|
||||
__all__ = ["init", "tables", "tool_registry", "verdict"]
|
||||
from .m4a_init import ( # noqa: F401
|
||||
load_pbl_agent_runtime_m4a,
|
||||
bootstrap,
|
||||
get_dbname,
|
||||
ddl,
|
||||
API_ROUTES,
|
||||
MODULE_NAME,
|
||||
)
|
||||
from .m4a import M4aApi, load_m4a, make_agent_ctx, make_user_ctx, make_admin_ctx # noqa: F401
|
||||
from .init import load_pbl_agent_runtime, self_check # noqa: F401
|
||||
from .m4a_registry import ( # noqa: F401
|
||||
seed_agents,
|
||||
seed_tools,
|
||||
list_agents,
|
||||
list_tools,
|
||||
get_tool,
|
||||
register_tool,
|
||||
set_tool_status,
|
||||
ENABLED_TOOL_CODES,
|
||||
DISABLED_TOOL_CODES,
|
||||
APPROVAL_REQUIRED_TOOLS,
|
||||
PERM_PLATFORM_ADMIN,
|
||||
)
|
||||
from .m4a_tables import ( # noqa: F401
|
||||
ensure_tables,
|
||||
TABLES,
|
||||
APPEND_ONLY_TABLES,
|
||||
all_sql,
|
||||
)
|
||||
from .m4a_kernel import ADJUDICATION_STEPS, TRACE_ELEMENTS, PblError # noqa: F401
|
||||
from .m4a_approval import MANDATORY_APPROVAL_TYPES # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
# 挂载入口
|
||||
"load_pbl_agent_runtime",
|
||||
"load_pbl_agent_runtime_m4a",
|
||||
"load_m4a",
|
||||
"bootstrap",
|
||||
"self_check",
|
||||
"get_dbname",
|
||||
"ddl",
|
||||
# 门面 / 上下文
|
||||
"M4aApi",
|
||||
"make_agent_ctx",
|
||||
"make_user_ctx",
|
||||
"make_admin_ctx",
|
||||
# 注册表
|
||||
"seed_agents",
|
||||
"seed_tools",
|
||||
"list_agents",
|
||||
"list_tools",
|
||||
"get_tool",
|
||||
"register_tool",
|
||||
"set_tool_status",
|
||||
"ENABLED_TOOL_CODES",
|
||||
"DISABLED_TOOL_CODES",
|
||||
"APPROVAL_REQUIRED_TOOLS",
|
||||
"PERM_PLATFORM_ADMIN",
|
||||
# 表 / 裁决常量
|
||||
"ensure_tables",
|
||||
"TABLES",
|
||||
"APPEND_ONLY_TABLES",
|
||||
"all_sql",
|
||||
"ADJUDICATION_STEPS",
|
||||
"TRACE_ELEMENTS",
|
||||
"MANDATORY_APPROVAL_TYPES",
|
||||
"PblError",
|
||||
"API_ROUTES",
|
||||
"MODULE_NAME",
|
||||
# 子模块(供高级用法直接 import)
|
||||
"init",
|
||||
"m4a",
|
||||
"m4a_init",
|
||||
"m4a_kernel",
|
||||
"m4a_tables",
|
||||
"m4a_registry",
|
||||
"m4a_adjudicate",
|
||||
"m4a_approval",
|
||||
"m4a_trace",
|
||||
"m4a_designer",
|
||||
"m4a_critic",
|
||||
"m4a_backend",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@ -1,366 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_agent_runtime.api —— Designer/Critic 运行时 + fail-closed 工具裁决(第13/14/28/31章,M4a/M4b)。
|
||||
|
||||
不可绕过的三条硬线(QC/PM 均要求可机械核对):
|
||||
1) **Critic 零写权限**:agent_type=critic 的 permission_mode 恒为 read,写类工具直接 DENIED;
|
||||
2) **无 pbl.publish**:DISABLED_TOOLS 注册契约但 enabled=0,裁决第 3 步即拒(含 pbl.publish);
|
||||
3) **四类强制人工审批**:审批未 approved 前,相关写工具在裁决第 6 步 DENIED,无旁路参数。
|
||||
|
||||
fail-closed 8 步裁决顺序(任一步不过 = 拒绝并留痕,绝不默认放行):
|
||||
① Agent 存在且启用 → ② 工具在注册表且 enabled → ③ 工具未被禁用(G6/pbl.publish) →
|
||||
④ Agent 权限模式允许该副作用级别 → ⑤ 参数过 JSON Schema 白名单 → ⑥ 需审批类已 approved →
|
||||
⑦ 租户归属校验 → ⑧ 执行并写 trace/tool_call 留痕。
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
from pbl_common.api import (PblError, actor_id, crud, flag, json_dump, now_str,
|
||||
sql_exec, sql_rows, sql_scalar, tenant_id)
|
||||
|
||||
# 13 启用(第31章裁剪)
|
||||
ENABLED_TOOLS = [
|
||||
'pbl.get', 'pbl.create', 'pbl.update', 'pbl.validate', 'pbl.compare', 'pbl.compile',
|
||||
'pbl.preview', 'kdb.search', 'kdb.get', 'runtime.get_state', 'runtime.playtest',
|
||||
'analytics.query', 'template.instantiate',
|
||||
]
|
||||
# 9 禁用(注册契约、零实现入口;G6 范围纪律)
|
||||
DISABLED_TOOLS = [
|
||||
'pbl.publish', 'kdb.add_candidate', 'kdb.propose_pattern', 'analytics.compare',
|
||||
'analytics.aggregate', 'experiment.create', 'experiment.run', 'showplace.publish',
|
||||
'sim.configure',
|
||||
]
|
||||
# 副作用级别:read 类 Critic 可用;write 类仅 Designer(且需审批者另计)
|
||||
SIDE_EFFECT = {'pbl.get': 'read', 'kdb.search': 'read', 'kdb.get': 'read',
|
||||
'runtime.get_state': 'read', 'analytics.query': 'read', 'pbl.preview': 'read',
|
||||
'pbl.compare': 'read',
|
||||
'pbl.create': 'write', 'pbl.update': 'write', 'pbl.validate': 'write',
|
||||
'pbl.compile': 'write', 'runtime.playtest': 'write',
|
||||
'template.instantiate': 'write'}
|
||||
# 14.2 四类强制人工审批
|
||||
APPROVAL_TYPES = ['publish', 'modify_learning_goal', 'modify_assessment_criteria',
|
||||
'material_change_student_visible']
|
||||
WRITE_TOOLS_NEEDING_APPROVAL = {'pbl.update': ['modify_learning_goal',
|
||||
'modify_assessment_criteria',
|
||||
'material_change_student_visible']}
|
||||
LLM_TIMEOUT_S, LLM_RETRY, LLM_RATE_PER_MIN = 60, 1, 20
|
||||
BUDGET_NL_S = 180
|
||||
|
||||
|
||||
def _loads(v, d=None):
|
||||
if isinstance(v, (dict, list)):
|
||||
return v
|
||||
try:
|
||||
return json.loads(v) if v else (d or {})
|
||||
except ValueError:
|
||||
return d or {}
|
||||
|
||||
|
||||
async def _tid():
|
||||
return await tenant_id()
|
||||
|
||||
|
||||
async def _agent(tid, code):
|
||||
rows = await sql_rows('SELECT * FROM `pbl_agent_def` WHERE `tenant_id` = ${t}$'
|
||||
' AND `code` = ${c}$ LIMIT 1', {'t': tid, 'c': code}, 'pbl')
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
async def _tool(tid, key):
|
||||
rows = await sql_rows('SELECT * FROM `pbl_tool_registry` WHERE `tenant_id` = ${t}$'
|
||||
' AND `tool_key` = ${k}$ LIMIT 1', {'t': tid, 'k': key}, 'pbl')
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
async def _trace_write(payload):
|
||||
"""7 要素轨迹落库(第28章),供 pbl_agent_trace_list 审计回放。"""
|
||||
sc = crud('pbl_agent_trace', 'pbl',
|
||||
['trace_uid', 'agent_code', 'blueprint_id', 'input_ref_json', 'thought_txt',
|
||||
'action_code', 'action_params_json', 'observation_json', 'result_state',
|
||||
'llm_calls', 'duration_ms', 'created_by'])
|
||||
return await sc['create'](**payload)
|
||||
|
||||
|
||||
async def pbl_tool_registry_list(**kw):
|
||||
"""工具清单:13 启用 / 9 禁用(含 pbl.publish),零实现入口可 grep 核对。"""
|
||||
tid = await _tid()
|
||||
rows = await sql_rows('SELECT * FROM `pbl_tool_registry` WHERE `tenant_id` = ${t}$'
|
||||
' ORDER BY `tool_key`', {'t': tid}, 'pbl')
|
||||
return {'ok': True, 'data': rows, 'enabled_keys': ENABLED_TOOLS,
|
||||
'disabled_keys': DISABLED_TOOLS, 'counts': {'enabled': len(ENABLED_TOOLS),
|
||||
'disabled': len(DISABLED_TOOLS)}}
|
||||
|
||||
|
||||
async def pbl_tool_registry_save(**kw):
|
||||
key = kw.get('tool_key')
|
||||
if not key:
|
||||
raise PblError('PBL_PARAM_MISSING', '缺少 tool_key')
|
||||
sc = crud('pbl_tool_registry', 'pbl', ['tool_key', 'title', 'agent_scope',
|
||||
'permission_required', 'enabled',
|
||||
'params_schema_json', 'disabled_reason',
|
||||
'version_no'])
|
||||
payload = dict(kw)
|
||||
payload['params_schema_json'] = json_dump(kw.get('params_schema_json'))
|
||||
payload.setdefault('version_no', 1)
|
||||
payload.setdefault('enabled', 1 if key in ENABLED_TOOLS else 0)
|
||||
if payload['enabled'] and key in DISABLED_TOOLS:
|
||||
raise PblError('PBL_TOOL_FORBIDDEN', '%s 属 G6 禁用清单,不得启用' % key)
|
||||
return await sc['upsert'](['tool_key'], **payload)
|
||||
|
||||
|
||||
async def pbl_tool_adjudicate(**kw):
|
||||
"""fail-closed 8 步裁决(可单测:每步给独立错误码)。"""
|
||||
tid = await _tid()
|
||||
code, tool_key = kw.get('agent_code'), kw.get('tool_key')
|
||||
params = _loads(kw.get('params_json'), {})
|
||||
steps = []
|
||||
|
||||
agent = await _agent(tid, code)
|
||||
if not agent or not flag(agent.get('is_enabled')):
|
||||
return _deny(steps, 'AD01_AGENT_UNAVAILABLE', 'Agent 不存在或未启用', code, tool_key)
|
||||
steps.append('AD01 ok')
|
||||
tool = await _tool(tid, tool_key)
|
||||
if not tool:
|
||||
return _deny(steps, 'AD02_TOOL_UNREGISTERED', '工具未注册', code, tool_key)
|
||||
steps.append('AD02 ok')
|
||||
if tool_key in DISABLED_TOOLS or not flag(tool.get('enabled')):
|
||||
return _deny(steps, 'AD03_TOOL_DISABLED',
|
||||
'工具禁用(G6 范围纪律):%s' % (tool.get('disabled_reason') or 'contract_only'),
|
||||
code, tool_key)
|
||||
steps.append('AD03 ok')
|
||||
level = SIDE_EFFECT.get(tool_key, 'write')
|
||||
if level == 'write' and (agent.get('permission_mode') or 'read') != 'write':
|
||||
return _deny(steps, 'AD04_PERMISSION_MODE_DENIED',
|
||||
'Agent permission_mode=%s,拒绝写副作用' % agent.get('permission_mode'),
|
||||
code, tool_key)
|
||||
steps.append('AD04 ok')
|
||||
bad = _schema_check(_loads(tool.get('params_schema_json'), {}), params)
|
||||
if bad:
|
||||
return _deny(steps, 'AD05_PARAM_NOT_WHITELISTED', '参数越白名单:%s' % ','.join(bad),
|
||||
code, tool_key)
|
||||
steps.append('AD05 ok')
|
||||
need = WRITE_TOOLS_NEEDING_APPROVAL.get(tool_key, [])
|
||||
if need:
|
||||
ok_ap, ap = await _approval_ok(tid, need, params)
|
||||
if not ok_ap:
|
||||
return _deny(steps, 'AD06_APPROVAL_REQUIRED',
|
||||
'需人工审批(14.2:%s)且无 approved 单' % '/'.join(need),
|
||||
code, tool_key, approval=ap)
|
||||
steps.append('AD06 ok(approved)')
|
||||
else:
|
||||
steps.append('AD06 n/a')
|
||||
if params.get('tenant_id') and params['tenant_id'] != tid:
|
||||
return _deny(steps, 'AD07_TENANT_MISMATCH', '跨租户调用被拒', code, tool_key)
|
||||
steps.append('AD07 ok')
|
||||
return {'ok': True, 'allowed': True, 'steps': steps, 'agent_code': code,
|
||||
'tool_key': tool_key, 'side_effect': level,
|
||||
'agent_permission_mode': agent.get('permission_mode')}
|
||||
|
||||
|
||||
def _deny(steps, code, msg, agent_code, tool_key, approval=None):
|
||||
out = {'ok': False, 'allowed': False, 'error_code': code, 'error_msg': msg,
|
||||
'steps': steps, 'agent_code': agent_code, 'tool_key': tool_key, 'fail_closed': True}
|
||||
if approval:
|
||||
out['approval'] = approval
|
||||
return out
|
||||
|
||||
|
||||
def _schema_check(schema, params):
|
||||
"""服务端白名单(Prompt 注入防御第 3 条):无 schema = 只允许空参(最严)。"""
|
||||
allowed = set((schema or {}).get('properties') or schema.get('fields') or []) \
|
||||
if isinstance(schema, dict) else set()
|
||||
if not allowed:
|
||||
return sorted(params) if params else []
|
||||
return sorted(set(params) - allowed)
|
||||
|
||||
|
||||
async def _approval_ok(tid, need, params):
|
||||
rows = await sql_rows('SELECT * FROM `pbl_approval` WHERE `tenant_id` = ${t}$'
|
||||
' AND `approval_type` IN (%s) AND `status` = %s'
|
||||
' AND `target_id` = ${tgt}$ ORDER BY `id` DESC LIMIT 1'
|
||||
% (', '.join(['${n%d}$' % i for i in range(len(need))]), "'approved'"),
|
||||
dict({'t': tid, 'tgt': params.get('blueprint_id') or params.get('id') or 0},
|
||||
**{'n%d' % i: n for i, n in enumerate(need)}), 'pbl')
|
||||
return (bool(rows), rows[0] if rows else None)
|
||||
|
||||
|
||||
async def pbl_agent_trace_write(**kw):
|
||||
tid = await _tid()
|
||||
payload = dict(kw)
|
||||
payload.setdefault('trace_uid', 'T%s%s' % (now_str().replace('-', '').replace(':', '')
|
||||
.replace(' ', ''), (kw.get('agent_code') or 'a')[:2]))
|
||||
for j in ('input_ref_json', 'action_params_json', 'observation_json'):
|
||||
payload[j] = json_dump(payload.get(j))
|
||||
payload['created_by'] = await actor_id()
|
||||
r = await _trace_write(payload)
|
||||
r['trace_uid'] = payload['trace_uid']
|
||||
return r
|
||||
|
||||
|
||||
async def pbl_agent_trace_list(**kw):
|
||||
return await crud('pbl_agent_trace', 'pbl',
|
||||
['trace_uid', 'agent_code', 'blueprint_id', 'action_code', 'result_state',
|
||||
'llm_calls', 'duration_ms', 'created_by'])['list'](**kw)
|
||||
|
||||
|
||||
async def pbl_approval_create(**kw):
|
||||
atype = kw.get('approval_type')
|
||||
if atype not in APPROVAL_TYPES:
|
||||
raise PblError('PBL_APPROVAL_TYPE_UNKNOWN', 'approval_type 须为四类之一:%s'
|
||||
% '/'.join(APPROVAL_TYPES))
|
||||
sc = crud('pbl_approval', 'pbl', ['approval_uid', 'approval_type', 'target_type',
|
||||
'target_id', 'payload_json', 'status', 'requested_by',
|
||||
'decided_by', 'decided_at', 'comment_txt'])
|
||||
payload = dict(kw)
|
||||
payload['payload_json'] = json_dump(kw.get('payload_json'))
|
||||
payload.setdefault('approval_uid', 'A%s' % hashlib.sha256(
|
||||
(atype + str(kw.get('target_id')) + now_str()).encode()).hexdigest()[:20])
|
||||
payload.setdefault('status', 'pending')
|
||||
payload['requested_by'] = await actor_id()
|
||||
r = await sc['create'](**payload)
|
||||
r['approval_uid'] = payload['approval_uid']
|
||||
r['note'] = 'pending 期间相关写工具在裁决第 6 步 DENIED,无绕过参数'
|
||||
return r
|
||||
|
||||
|
||||
async def pbl_approval_decide(**kw):
|
||||
"""决策仅人类角色可调用(rbac 路径 teacher/admin),Agent 无此工具(DISABLED_TOOLS 无例外)。"""
|
||||
tid = await _tid()
|
||||
status = kw.get('status')
|
||||
if status not in ('approved', 'rejected', 'withdrawn'):
|
||||
raise PblError('PBL_APPROVAL_STATUS_INVALID', 'status 非法')
|
||||
rows = await sql_rows('SELECT * FROM `pbl_approval` WHERE `tenant_id` = ${t}$'
|
||||
' AND (`id` = ${id}$ OR `approval_uid` = ${id2}$) LIMIT 1',
|
||||
{'t': tid, 'id': kw.get('id') or 0, 'id2': kw.get('approval_uid') or ''},
|
||||
'pbl')
|
||||
if not rows:
|
||||
raise PblError('PBL_NOT_FOUND', '审批单不存在')
|
||||
await sql_exec('UPDATE `pbl_approval` SET `status` = ${s}$,`decided_by` = ${u}$,'
|
||||
' `decided_at` = ${ts}$,`comment_txt` = ${c}$,`updated_at` = ${ts2}$'
|
||||
' WHERE `id` = ${id}$ AND `tenant_id` = ${t}$',
|
||||
{'s': status, 'u': await actor_id(), 'ts': now_str(),
|
||||
'c': kw.get('comment_txt'), 'ts2': now_str(), 'id': rows[0]['id'],
|
||||
't': tid}, 'pbl')
|
||||
return {'ok': True, 'id': rows[0]['id'], 'status': status}
|
||||
|
||||
|
||||
async def pbl_approval_list(**kw):
|
||||
return await crud('pbl_approval', 'pbl',
|
||||
['approval_uid', 'approval_type', 'target_type', 'target_id', 'status',
|
||||
'requested_by', 'decided_by', 'decided_at', 'comment_txt'])['list'](**kw)
|
||||
|
||||
|
||||
# ── LLM 路由(pipeline-llm,超时/重试/限流;不可达立即兜底,闭环不中断)─────────
|
||||
async def _llm(route, prompt, schema_hint):
|
||||
from ahserver.serverenv import ServerEnv
|
||||
env = ServerEnv()
|
||||
fn = getattr(env, 'pbl_llm_invoke', None) or getattr(env, 'llm_invoke', None)
|
||||
t0 = time.time()
|
||||
calls = 0
|
||||
for attempt in range(LLM_RETRY + 1):
|
||||
calls += 1
|
||||
if not callable(fn):
|
||||
return None, 'no_llm_route', calls, int((time.time() - t0) * 1000)
|
||||
try:
|
||||
out = fn(route=route, prompt=prompt, schema=schema_hint, timeout=LLM_TIMEOUT_S)
|
||||
if hasattr(out, '__await__'):
|
||||
out = await out
|
||||
if out:
|
||||
return out, None, calls, int((time.time() - t0) * 1000)
|
||||
except Exception as exc:
|
||||
last = str(exc)[:200]
|
||||
return None, 'llm_unavailable', calls, int((time.time() - t0) * 1000)
|
||||
|
||||
|
||||
async def pbl_agent_designer_run(**kw):
|
||||
"""自然语言 → 结构化 Blueprint(LLM 不可用 → 模板兜底,generation_source=template_fallback)。"""
|
||||
tid = await _tid()
|
||||
nl = kw.get('input_txt') or kw.get('prompt') or ''
|
||||
agent = await _agent(tid, kw.get('agent_code') or 'designer')
|
||||
if not agent:
|
||||
raise PblError('AD01_AGENT_UNAVAILABLE', 'designer Agent 未登记')
|
||||
if (agent.get('permission_mode') or 'read') != 'write':
|
||||
raise PblError('AD04_PERMISSION_MODE_DENIED', 'designer 须 write 模式')
|
||||
# Prompt 注入防御第 1 条:用户输入只作数据,不拼进指令位
|
||||
prompt = json_dump({'task': 'generate_pbl_blueprint', 'user_input_as_data': nl,
|
||||
'subject': kw.get('subject'), 'grade': kw.get('grade')})
|
||||
raw, err, calls, ms = await _llm(agent.get('model_route') or 'pipeline-llm', prompt,
|
||||
'pbl.blueprint.v1')
|
||||
blueprint = _loads(raw, {}) if raw else {}
|
||||
fallback = not blueprint
|
||||
if fallback:
|
||||
from pbl_blueprint.api import pbl_template_instantiate
|
||||
tpl = kw.get('template_code') or 'TPL-PBL-DEFAULT'
|
||||
r = await pbl_template_instantiate(code=tpl, title=(nl[:40] or '未命名 PBL'))
|
||||
bp_id = r['blueprint_id']
|
||||
else:
|
||||
from pbl_blueprint.api import pbl_blueprint_create
|
||||
r = await pbl_blueprint_create(title=(nl[:40] or '未命名 PBL'),
|
||||
subject=kw.get('subject'), grade=kw.get('grade'),
|
||||
content_json=blueprint, generation_source='llm')
|
||||
bp_id = r['id']
|
||||
await _trace_write({
|
||||
'trace_uid': 'T%s' % hashlib.sha256((nl + now_str()).encode()).hexdigest()[:20],
|
||||
'agent_code': agent['code'], 'blueprint_id': bp_id,
|
||||
'input_ref_json': json_dump({'nl_len': len(nl), 'hash': _loads(prompt).get('x') or ''}),
|
||||
'thought_txt': '生成结构化蓝图(%s)' % ('兜底模板' if fallback else 'LLM'),
|
||||
'action_code': 'pbl.create', 'action_params_json': json_dump({'subject': kw.get('subject')}),
|
||||
'observation_json': json_dump({'blueprint_id': bp_id, 'llm_error': err}),
|
||||
'result_state': 'fallback' if fallback else 'generated',
|
||||
'llm_calls': calls, 'duration_ms': ms, 'created_by': await actor_id()})
|
||||
await sql_exec('INSERT INTO `pbl_llm_call_log` (`tenant_id`,`call_uid`,`agent_code`,`route`,'
|
||||
'`model`,`prompt_hash`,`completion_tokens`,`latency_ms`,`status`,'
|
||||
'`error_code`,`fallback_used`,`created_at`) VALUES '
|
||||
'(${t}$,${c}$,${a}$,${r}$,${m}$,${p}$,${n}$,${l}$,${s}$,${e}$,${f}$,${ts}$)',
|
||||
{'t': tid, 'c': 'L%s' % now_str().replace('-', '').replace(':', '')
|
||||
.replace(' ', ''), 'a': agent['code'],
|
||||
'r': agent.get('model_route'), 'm': '', 'p': hashlib.sha256(
|
||||
prompt.encode()).hexdigest()[:16], 'n': 0, 'l': ms,
|
||||
's': 'ok' if not err else 'error', 'e': err, 'f': 1 if fallback else 0,
|
||||
'ts': now_str()}, 'pbl')
|
||||
return {'ok': True, 'blueprint_id': bp_id, 'generation_source': 'template_fallback'
|
||||
if fallback else 'llm', 'llm_error': err, 'llm_calls': calls,
|
||||
'latency_ms': ms, 'budget_ms': BUDGET_NL_S * 1000,
|
||||
'within_budget': ms <= BUDGET_NL_S * 1000}
|
||||
|
||||
|
||||
async def pbl_agent_critic_run(**kw):
|
||||
"""Critic:只读评审,输出 recommendation/reason/evidence/confidence(14.3 可解释)。"""
|
||||
tid = await _tid()
|
||||
agent = await _agent(tid, kw.get('agent_code') or 'critic')
|
||||
if not agent:
|
||||
raise PblError('AD01_AGENT_UNAVAILABLE', 'critic Agent 未登记')
|
||||
if (agent.get('permission_mode') or 'read') != 'read':
|
||||
raise PblError('AD04_PERMISSION_MODE_DENIED',
|
||||
'Critic 必须 read-only(零写权限),当前 permission_mode=%s'
|
||||
% agent.get('permission_mode'))
|
||||
bp_id = kw.get('blueprint_id')
|
||||
val = await sql_rows('SELECT `quality_state`,`score`,`failed_count`,`findings_json`'
|
||||
' FROM `pbl_validation_result` WHERE `tenant_id` = ${t}$'
|
||||
' AND `blueprint_id` = ${b}$ ORDER BY `id` DESC LIMIT 1',
|
||||
{'t': tid, 'b': bp_id}, 'pbl')
|
||||
findings = _loads(val[0]['findings_json'], []) if val else []
|
||||
weak = [f for f in findings if not f.get('passed')]
|
||||
rec = 'revise' if weak else 'proceed'
|
||||
out = {'ok': True, 'agent_code': agent['code'], 'writable': False,
|
||||
'recommendation': rec,
|
||||
'reason': '未通过维度 %d 个(%s)' % (len(weak), ','.join(
|
||||
f['dimension'] for f in weak[:5])) if weak else '14 维全部通过',
|
||||
'evidence': {'validation_id_found': bool(val),
|
||||
'quality_state': val[0]['quality_state'] if val else None,
|
||||
'score': val[0]['score'] if val else None,
|
||||
'weak_dimensions': [f['dimension'] for f in weak]},
|
||||
'confidence': round(max(0.0, 1.0 - 0.12 * len(weak)), 2),
|
||||
'suggestions': [f.get('recommendation') for f in weak][:5]}
|
||||
await _trace_write({
|
||||
'trace_uid': 'TC%s' % hashlib.sha256(('%s%s' % (bp_id, now_str())).encode())
|
||||
.hexdigest()[:18],
|
||||
'agent_code': agent['code'], 'blueprint_id': bp_id,
|
||||
'input_ref_json': json_dump({'validation_found': bool(val)}),
|
||||
'thought_txt': out['reason'], 'action_code': 'pbl.get',
|
||||
'action_params_json': json_dump({'blueprint_id': bp_id}),
|
||||
'observation_json': json_dump(out['evidence']),
|
||||
'result_state': rec, 'llm_calls': 0, 'duration_ms': 0,
|
||||
'created_by': await actor_id()})
|
||||
return out
|
||||
@ -1,135 +1,470 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_agent_runtime 挂载入口(M4a/M4b)。
|
||||
"""pbl_agent_runtime 挂载入口(M4a 唯一实现)。
|
||||
|
||||
load_pbl_agent_runtime(env=None):
|
||||
1. ensure_tables 建 4 表(幂等)
|
||||
2. **挂载即执行 self_check()**:22=13+9 数量不符、或写保护域存在启用写工具、
|
||||
或 6 探针裁决不符 → 抛 RuntimeError,应用启动失败(fail-closed,绝不带病上线)
|
||||
3. 注册 tools / enabled_tools / disabled_tools / api(judge/call_tool/verdict_stats/self_check)
|
||||
4. 返回 default_verdict(= 'DENY')
|
||||
历史说明(QC 退回 #7 处置):
|
||||
旧「九关裁决 + 4 表」实现(tables.py / tool_registry.py / verdict.py / api.py)
|
||||
已**删除**,本文件不再引用;M4a(m4a_*.py,8 步裁决 + 5 表)是本模块唯一实现,
|
||||
`load_pbl_agent_runtime()` 与 `load_pbl_agent_runtime_m4a()` 均委托 M4a 装配。
|
||||
|
||||
对齐 docs/01-design/agent-tool-contract.md(13 启用 / 9 禁用,fail-closed)。
|
||||
load_pbl_agent_runtime(env=None, tenant_id=None):
|
||||
1) ensure_tables —— 建 5 表(幂等;pbl_agent_trace / pbl_agent_trace_stage 为 append-only)
|
||||
2) seed_agents(designer / critic,critic write_allowed=0)+ seed_tools(13 启用 / 9 禁用)
|
||||
3) **挂载即 self_check()**:数量契约、Critic 零写、8 步裁决链、四类审批、默认裁决 DENY
|
||||
任一不过 → 抛 RuntimeError,宿主应用启动失败(fail-closed,绝不带病上线)
|
||||
4) 向 ServerEnv 注册 wwwroot/api/*.dspy 所需的 8 个处理函数 + 只读契约
|
||||
5) 返回 default_verdict(= 'DENY')
|
||||
|
||||
后端(sqlor/ahserver)未挂载时自动降级 MemoryStore:建表/seed/裁决/审批全链路可用,
|
||||
数据仅存于进程内(重启即失),不抛错——便于自测与离线演示(US-05 兜底同源策略)。
|
||||
"""
|
||||
|
||||
from . import tables as _tables
|
||||
from . import tool_registry as _registry
|
||||
from . import verdict as _verdict
|
||||
from .m4a_kernel import (
|
||||
ADJUDICATION_STEPS,
|
||||
TRACE_ELEMENTS,
|
||||
PblError,
|
||||
TenantContext,
|
||||
ACTOR_SYSTEM,
|
||||
get_store,
|
||||
)
|
||||
from .m4a_tables import TABLES, APPEND_ONLY_TABLES, ensure_tables
|
||||
from .m4a_registry import (
|
||||
ENABLED_TOOL_CODES,
|
||||
DISABLED_TOOL_CODES,
|
||||
APPROVAL_REQUIRED_TOOLS,
|
||||
PERM_PLATFORM_ADMIN,
|
||||
list_agents,
|
||||
list_tools,
|
||||
get_tool,
|
||||
register_tool,
|
||||
set_tool_status,
|
||||
seed_agents,
|
||||
seed_tools,
|
||||
)
|
||||
from .m4a_adjudicate import adjudicate, adjudicate_report, explain_chain
|
||||
from .m4a_approval import MANDATORY_APPROVAL_TYPES
|
||||
from .m4a_trace import get_trace, list_traces
|
||||
from .m4a import M4aApi, load_m4a, make_admin_ctx
|
||||
from .m4a_init import MODULE_NAME, get_dbname
|
||||
|
||||
MODULE = "pbl_agent_runtime"
|
||||
EXPECTED_TOTAL = 22
|
||||
import functools
|
||||
|
||||
DEFAULT_VERDICT = "DENY" # fail-closed:任何异常/未知一律拒绝
|
||||
EXPECTED_ENABLED = 13
|
||||
EXPECTED_DISABLED = 9
|
||||
EXPECTED_STEPS = 8
|
||||
EXPECTED_APPROVAL_TYPES = 4
|
||||
EXPECTED_TABLES = 5
|
||||
POLICY_VERSION = "m4a-1.0.0"
|
||||
AGENTS = ("designer", "critic")
|
||||
|
||||
|
||||
def ensure_tables(env=None, sor=None):
|
||||
"""幂等建 4 表。返回 (ok_list, bad_list)。"""
|
||||
return _tables.ensure_tables(env=env, sor=sor)
|
||||
# ---------------------------------------------------------------------------
|
||||
# 自检(挂载即执行,不过即抛)
|
||||
# ---------------------------------------------------------------------------
|
||||
def self_check(tenant_id=None, store=None, run_probes=True):
|
||||
"""M4a 契约自检。返回 (all_ok, msgs)。
|
||||
|
||||
|
||||
def self_check(env=None, run_probes=True):
|
||||
"""模块自检:数量契约 + 写保护域 + 裁决探针。返回 (all_ok, msgs)。"""
|
||||
校验项:
|
||||
C1 工具数量契约 22 = 13 启用 + 9 禁用
|
||||
C2 自动发布类工具必须禁用(blueprint.publish_auto / marketplace.create_listing)
|
||||
C3 Critic 零写权限(write_allowed=0 且无任何写工具 allowed_agents 含 critic)
|
||||
C4 裁决链恰为 8 步且顺序为 S1..S8
|
||||
C5 四类强制人工审批齐备
|
||||
C6 5 张表齐备且 append-only 表带标记
|
||||
C7 默认裁决 = DENY
|
||||
C8 探针:禁用工具/未注册工具/Critic 写 三类调用必须被拒(run_probes=True 时)
|
||||
"""
|
||||
msgs = []
|
||||
all_ok = True
|
||||
st = store or get_store()
|
||||
tid = tenant_id or "__platform__"
|
||||
admin = make_admin_ctx(tid)
|
||||
|
||||
tools = _registry.all_tools()
|
||||
enabled = [t for t in tools if t.get("enabled")]
|
||||
disabled = [t for t in tools if not t.get("enabled")]
|
||||
if len(tools) != EXPECTED_TOTAL:
|
||||
tools = list_tools(ctx=admin, store=st)
|
||||
enabled = [t for t in tools if t.get("status") == "enabled"]
|
||||
disabled = [t for t in tools if t.get("status") == "disabled"]
|
||||
|
||||
# C1
|
||||
if len(tools) != EXPECTED_ENABLED + EXPECTED_DISABLED:
|
||||
all_ok = False
|
||||
msgs.append("工具总数=%d 应为 %d" % (len(tools), EXPECTED_TOTAL))
|
||||
msgs.append("FAIL C1 工具总数=%d 应为 %d"
|
||||
% (len(tools), EXPECTED_ENABLED + EXPECTED_DISABLED))
|
||||
if len(enabled) != EXPECTED_ENABLED:
|
||||
all_ok = False
|
||||
msgs.append("启用工具=%d 应为 %d" % (len(enabled), EXPECTED_ENABLED))
|
||||
msgs.append("FAIL C1 启用工具=%d 应为 %d" % (len(enabled), EXPECTED_ENABLED))
|
||||
if len(disabled) != EXPECTED_DISABLED:
|
||||
all_ok = False
|
||||
msgs.append("禁用工具=%d 应为 %d" % (len(disabled), EXPECTED_DISABLED))
|
||||
msgs.append("工具契约:%d = %d 启用 + %d 禁用" % (len(tools), len(enabled), len(disabled)))
|
||||
msgs.append("FAIL C1 禁用工具=%d 应为 %d" % (len(disabled), EXPECTED_DISABLED))
|
||||
msgs.append("C1 工具契约:%d = %d 启用 + %d 禁用"
|
||||
% (len(tools), len(enabled), len(disabled)))
|
||||
|
||||
reg_ok, reg_msgs = _registry.self_check()
|
||||
if not reg_ok:
|
||||
all_ok = False
|
||||
msgs.extend(reg_msgs)
|
||||
|
||||
tbl_ok, tbl_msgs = _tables.self_check()
|
||||
if not tbl_ok:
|
||||
all_ok = False
|
||||
msgs.extend(tbl_msgs)
|
||||
|
||||
if run_probes:
|
||||
v_ok, v_msgs = _verdict.runtime_self_check()
|
||||
if not v_ok:
|
||||
# C2 自动发布/自动改课必须禁用
|
||||
must_disabled = ("blueprint.publish_auto", "curriculum.modify_auto",
|
||||
"marketplace.create_listing", "kdb.write", "billing.charge")
|
||||
codes_disabled = set(DISABLED_TOOL_CODES)
|
||||
for code in must_disabled:
|
||||
if code not in codes_disabled:
|
||||
all_ok = False
|
||||
msgs.extend(v_msgs)
|
||||
msgs.append("FAIL C2 %s 必须处于禁用清单" % code)
|
||||
msgs.append("C2 高危工具禁用:%s" % ", ".join(must_disabled))
|
||||
|
||||
if _verdict.DEFAULT_VERDICT != "DENY":
|
||||
# C3 Critic 零写
|
||||
agents = list_agents(ctx=admin, store=st)
|
||||
by_code = {a.get("agent_code"): a for a in agents}
|
||||
if set(by_code) != set(AGENTS):
|
||||
all_ok = False
|
||||
msgs.append("DEFAULT_VERDICT=%r 必须为 'DENY'(fail-closed)"
|
||||
% _verdict.DEFAULT_VERDICT)
|
||||
msgs.append("FAIL C3 Agent 定义=%s 应为 %s" % (sorted(by_code), list(AGENTS)))
|
||||
critic = by_code.get("critic") or {}
|
||||
if critic and int(critic.get("write_allowed") or 0) != 0:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C3 critic.write_allowed=%r 必须为 0(14.1 零写权限)"
|
||||
% critic.get("write_allowed"))
|
||||
write_tools_for_critic = [
|
||||
t.get("tool_code") for t in enabled
|
||||
if int(t.get("write_operation") or 0) == 1
|
||||
and "critic" in (t.get("allowed_agents") or [])
|
||||
]
|
||||
if write_tools_for_critic:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C3 critic 持有写工具:%s" % write_tools_for_critic)
|
||||
msgs.append("C3 Critic 零写权限:write_allowed=0,写工具授权数=0")
|
||||
|
||||
# C4 8 步裁决链
|
||||
steps = [s[0] for s in ADJUDICATION_STEPS]
|
||||
if len(steps) != EXPECTED_STEPS or steps != ["S%d" % i for i in range(1, 9)]:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C4 裁决链=%s 应为 S1..S8(8 步顺序固定)" % steps)
|
||||
else:
|
||||
msgs.append("C4 fail-closed 裁决链 8 步:%s"
|
||||
% " → ".join("%s %s" % (s[0], s[1]) for s in ADJUDICATION_STEPS))
|
||||
|
||||
# C5 四类审批
|
||||
if len(MANDATORY_APPROVAL_TYPES) != EXPECTED_APPROVAL_TYPES:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C5 强制审批类型=%d 应为 %d"
|
||||
% (len(MANDATORY_APPROVAL_TYPES), EXPECTED_APPROVAL_TYPES))
|
||||
appr_tools = [t.get("tool_code") for t in enabled
|
||||
if int(t.get("require_approval") or 0) == 1]
|
||||
if set(appr_tools) != set(APPROVAL_REQUIRED_TOOLS):
|
||||
all_ok = False
|
||||
msgs.append("FAIL C5 require_approval 工具=%s 应为 %s"
|
||||
% (appr_tools, list(APPROVAL_REQUIRED_TOOLS)))
|
||||
msgs.append("C5 四类强制人工审批:%s;需审批工具=%s"
|
||||
% (", ".join(MANDATORY_APPROVAL_TYPES), ", ".join(appr_tools)))
|
||||
|
||||
# C6 5 表 + append-only
|
||||
tbl_names = [t.get("tblname") for t in TABLES]
|
||||
if len(tbl_names) != EXPECTED_TABLES:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C6 表数=%d 应为 %d(%s)"
|
||||
% (len(tbl_names), EXPECTED_TABLES, tbl_names))
|
||||
for name in ("pbl_agent_trace", "pbl_agent_trace_stage"):
|
||||
if name not in APPEND_ONLY_TABLES:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C6 %s 必须标记 append-only" % name)
|
||||
msgs.append("C6 数据表 %d 张:%s(append-only:%s)"
|
||||
% (len(tbl_names), ", ".join(tbl_names), ", ".join(APPEND_ONLY_TABLES)))
|
||||
|
||||
# C7 默认裁决
|
||||
if DEFAULT_VERDICT != "DENY":
|
||||
all_ok = False
|
||||
msgs.append("FAIL C7 DEFAULT_VERDICT=%r 必须为 'DENY'" % DEFAULT_VERDICT)
|
||||
else:
|
||||
msgs.append("C7 默认裁决 = DENY(fail-closed)")
|
||||
|
||||
# C8 探针
|
||||
if run_probes:
|
||||
probes = [
|
||||
("designer", "blueprint.publish_auto", "禁用工具(自动发布)"),
|
||||
("designer", "pbl.publish", "未注册工具(白名单外 default-deny)"),
|
||||
("critic", "blueprint.update", "Critic 写蓝图(14.1)"),
|
||||
("critic", "publish.request", "Critic 发布(14.1)"),
|
||||
("designer", "compile.trigger", "未审批编译执行(S7 审批门)"),
|
||||
]
|
||||
for agent_code, tool_code, why in probes:
|
||||
step = code = None
|
||||
try:
|
||||
rep = adjudicate_report(admin, agent_code, tool_code, {}, store=st)
|
||||
# Verdict.to_dict():allowed=False 即拒绝(fail-closed 语义)
|
||||
denied = rep.get("allowed") is False
|
||||
step = rep.get("step_reached")
|
||||
code = rep.get("reason_code")
|
||||
except PblError as e:
|
||||
denied, code = True, e.code
|
||||
except Exception: # noqa: BLE001 —— 任何异常都视为拒绝(fail-closed)
|
||||
denied, code = True, "EXCEPTION_AS_DENY"
|
||||
if not denied:
|
||||
all_ok = False
|
||||
msgs.append("FAIL C8 探针未被拒:%s 调 %s(%s)" % (agent_code, tool_code, why))
|
||||
else:
|
||||
msgs.append("C8 探针拒绝:%s → %s @%s %s(%s)"
|
||||
% (agent_code, tool_code, step, code, why))
|
||||
|
||||
msgs.append("轨迹 7 要素:%s" % ", ".join(TRACE_ELEMENTS))
|
||||
if all_ok:
|
||||
msgs.append("SELF_CHECK %s: PASS %d/%d" % (MODULE, len(tools), len(tools)))
|
||||
msgs.append("SELF_CHECK %s: PASS(%d 工具 / %d 表 / %d 步 / %d 类审批)"
|
||||
% (MODULE_NAME, len(tools), len(tbl_names),
|
||||
len(ADJUDICATION_STEPS), len(MANDATORY_APPROVAL_TYPES)))
|
||||
return all_ok, msgs
|
||||
|
||||
|
||||
def _wrap(fn):
|
||||
"""dspy 处理函数统一包装:PblError → 结构化拒绝响应(fail-closed,不抛 500)。
|
||||
|
||||
任何未预期异常同样按拒绝返回(default_verdict=DENY 同源策略),
|
||||
并带 error_code=PBL_E_FORBIDDEN,避免异常细节泄漏到前端。
|
||||
"""
|
||||
@functools.wraps(fn)
|
||||
async def _inner(**params_kw):
|
||||
try:
|
||||
data = await fn(**params_kw)
|
||||
except PblError as e:
|
||||
return dict(e.to_dict(), ok=False, default_verdict=DEFAULT_VERDICT)
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"ok": False, "error_code": "PBL_E_FORBIDDEN",
|
||||
"error_message": "内部异常,按 fail-closed 拒绝:%s" % type(e).__name__,
|
||||
"default_verdict": DEFAULT_VERDICT}
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("ok", True)
|
||||
return data
|
||||
return _inner
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dspy 处理函数(wwwroot/api/*.dspy 直接调用,名称必须与 dspy 内一致)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _tid(params_kw):
|
||||
tid = (params_kw or {}).get("tenant_id")
|
||||
if not tid:
|
||||
# fail-closed:租户上下文缺失即拒绝(S1)
|
||||
raise PblError("PBL_E_TENANT_MISSING", "tenant_id 强制打头,缺失即拒绝")
|
||||
return tid
|
||||
|
||||
|
||||
def _api(params_kw):
|
||||
return load_m4a(tenant_id=_tid(params_kw))
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_agent_designer_run(**params_kw):
|
||||
"""US-01/03/04:Designer 生成 / 修改 / 澄清(action 分派,缺省 generate)。"""
|
||||
api = _api(params_kw)
|
||||
action = params_kw.get("action", "generate")
|
||||
if action == "modify":
|
||||
return api.designer_modify(params_kw.get("blueprint_id"),
|
||||
params_kw.get("instruction"),
|
||||
session_no=params_kw.get("session_no"),
|
||||
version_no=params_kw.get("version_no"))
|
||||
if action == "clarify":
|
||||
return api.designer_clarify(params_kw.get("blueprint_id"),
|
||||
params_kw.get("missing_fields") or [],
|
||||
session_no=params_kw.get("session_no"),
|
||||
round_no=params_kw.get("round_no"))
|
||||
return api.designer_generate(params_kw.get("intent_text") or "",
|
||||
owner_teacher_id=params_kw.get("owner_teacher_id"),
|
||||
class_id=params_kw.get("class_id"),
|
||||
session_no=params_kw.get("session_no"))
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_agent_critic_run(**params_kw):
|
||||
"""US-18:Critic 只读评审(零写权限,写操作一律被 S6 拒)。"""
|
||||
api = _api(params_kw)
|
||||
if params_kw.get("trace_no"):
|
||||
return api.get_critic_report(params_kw["trace_no"])
|
||||
blueprint_id = params_kw.get("blueprint_id")
|
||||
if not blueprint_id:
|
||||
# 后端(pbl_blueprint 权威服务)未挂载 / Designer 兜底提案尚未落库时,
|
||||
# Critic 无可评审对象:返回结构化降级说明(不抛 500,不伪造评审结论)
|
||||
return {"ok": False, "degraded": True,
|
||||
"error_code": "PBL_E_BACKEND_UNAVAILABLE",
|
||||
"error_message": "blueprint_id 缺失或蓝图后端未挂载,Critic 无可评审对象;"
|
||||
"Designer 兜底提案落 pending_backend,待后端挂载后重试",
|
||||
"agent_code": "critic", "write_allowed": 0}
|
||||
return api.critic_review(blueprint_id,
|
||||
version_no=params_kw.get("version_no"),
|
||||
session_no=params_kw.get("session_no"))
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_tool_adjudicate(**params_kw):
|
||||
"""fail-closed 8 步裁决预检(只裁不执行)。tool_code 为空 → 返回裁决链说明。"""
|
||||
api = _api(params_kw)
|
||||
tool_code = params_kw.get("tool_code")
|
||||
if not tool_code:
|
||||
return {"steps": explain_chain(), "default_verdict": DEFAULT_VERDICT,
|
||||
"policy_version": POLICY_VERSION}
|
||||
return api.adjudicate(params_kw.get("agent_code") or "designer", tool_code,
|
||||
args=params_kw.get("args") or {})
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_agent_trace_list(**params_kw):
|
||||
"""F-AG-04:轨迹查询(可查不可篡改)。"""
|
||||
api = _api(params_kw)
|
||||
if params_kw.get("trace_no"):
|
||||
return {"trace": api.get_trace(params_kw["trace_no"]),
|
||||
"completeness": api.trace_completeness(params_kw["trace_no"])}
|
||||
return api.list_traces(params_kw.get("filters") or {},
|
||||
page=int(params_kw.get("page") or 1),
|
||||
size=int(params_kw.get("size") or 20))
|
||||
|
||||
|
||||
def _trace_write(api, action, agent_code, params_kw):
|
||||
from . import m4a_trace as _t
|
||||
ctx = api.agent_ctx(agent_code)
|
||||
if action == "start":
|
||||
return _t.start_trace(agent_code, session_no=params_kw.get("session_no"),
|
||||
input_context=params_kw.get("input_context"),
|
||||
ctx=ctx, store=api.store)
|
||||
if action == "append":
|
||||
return _t.append_trace(params_kw.get("trace_no"),
|
||||
params_kw.get("stage") or params_kw.get("element"),
|
||||
params_kw.get("payload") or {},
|
||||
ctx=ctx, store=api.store)
|
||||
raise PblError("PBL_E_VALIDATION",
|
||||
"action 非法:%r(append-only 仅支持 start / append)" % action)
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_approval_create(**params_kw):
|
||||
"""T10:Agent 发起人工审批提案(Agent 不可自批)。"""
|
||||
api = _api(params_kw)
|
||||
return api.request_approval(
|
||||
action_type=params_kw.get("action_type"),
|
||||
object_id=params_kw.get("object_id"),
|
||||
agent_code=params_kw.get("agent_code") or "designer",
|
||||
tool_code=params_kw.get("tool_code"),
|
||||
object_type=params_kw.get("object_type"),
|
||||
action_payload=params_kw.get("action_payload") or {},
|
||||
approver_id=params_kw.get("approver_id"),
|
||||
trace_no=params_kw.get("trace_no"),
|
||||
)
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_approval_decide(**params_kw):
|
||||
"""14.2:审批裁决 —— 仅 actor_type=user(人类)可调,Agent 调用被拒。"""
|
||||
api = _api(params_kw)
|
||||
return api.decide_approval(params_kw.get("approval_no"),
|
||||
params_kw.get("status"),
|
||||
params_kw.get("user_id"),
|
||||
comment=params_kw.get("comment"))
|
||||
|
||||
|
||||
@_wrap
|
||||
async def pbl_approval_list(**params_kw):
|
||||
"""待办审批工作台(approver_id 必填)。"""
|
||||
api = _api(params_kw)
|
||||
approver_id = params_kw.get("approver_id")
|
||||
if not approver_id:
|
||||
raise PblError("PBL_E_VALIDATION", "approver_id 必填")
|
||||
return api.list_pending_approvals(approver_id)
|
||||
|
||||
|
||||
# 修正 pbl_agent_trace_write 的 start 分支(避免占位返回)
|
||||
@_wrap
|
||||
async def pbl_agent_trace_write(**params_kw):
|
||||
"""轨迹写入(append-only)。action=start 开轨迹;action=append 追加 7 要素之一。"""
|
||||
api = _api(params_kw)
|
||||
return _trace_write(api, params_kw.get("action", "append"),
|
||||
params_kw.get("agent_code") or "designer", params_kw)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 对外契约
|
||||
# ---------------------------------------------------------------------------
|
||||
def api():
|
||||
"""对外 API 契约(供应用/其它模块调用)。"""
|
||||
"""模块对外 API 契约(供宿主应用 / 其它模块经 ServerEnv 调用)。"""
|
||||
return {
|
||||
"judge": _verdict.judge,
|
||||
"call_tool": _verdict.call_tool,
|
||||
"verdict_stats": _verdict.verdict_stats,
|
||||
"adjudicate": adjudicate,
|
||||
"adjudicate_report": adjudicate_report,
|
||||
"adjudication_steps": explain_chain,
|
||||
"list_tools": list_tools,
|
||||
"get_tool": get_tool,
|
||||
"register_tool": register_tool,
|
||||
"set_tool_status": set_tool_status,
|
||||
"list_agents": list_agents,
|
||||
"get_trace": get_trace,
|
||||
"list_traces": list_traces,
|
||||
"self_check": self_check,
|
||||
"get_tool": _registry.get_tool,
|
||||
"all_tools": _registry.all_tools,
|
||||
"ensure_tables": ensure_tables,
|
||||
"POLICY_VERSION": _verdict.POLICY_VERSION,
|
||||
"MAX_CALLS_PER_RUN": _verdict.MAX_CALLS_PER_RUN,
|
||||
"PblError": _verdict.PblError,
|
||||
"load_m4a": load_m4a,
|
||||
"M4aApi": M4aApi,
|
||||
"PblError": PblError,
|
||||
"DEFAULT_VERDICT": DEFAULT_VERDICT,
|
||||
"POLICY_VERSION": POLICY_VERSION,
|
||||
"ADJUDICATION_STEPS": ADJUDICATION_STEPS,
|
||||
"MANDATORY_APPROVAL_TYPES": MANDATORY_APPROVAL_TYPES,
|
||||
"ENABLED_TOOL_CODES": ENABLED_TOOL_CODES,
|
||||
"DISABLED_TOOL_CODES": DISABLED_TOOL_CODES,
|
||||
}
|
||||
|
||||
|
||||
def load_pbl_agent_runtime(env=None):
|
||||
"""挂载入口:建表 → 自检(不过即抛)→ 注册契约 → 返回 default_verdict。"""
|
||||
def load_pbl_agent_runtime(env=None, tenant_id=None, force_memory=False,
|
||||
do_seed=True, run_self_check=True):
|
||||
"""宿主应用标准挂载入口(委托 M4a 装配)。
|
||||
|
||||
返回 default_verdict('DENY')。自检不过抛 RuntimeError(fail-closed)。
|
||||
"""
|
||||
srv = env
|
||||
if srv is None:
|
||||
try:
|
||||
from ahserver.serverenv import ServerEnv
|
||||
srv = ServerEnv()
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001 —— 无宿主环境时降级 MemoryStore
|
||||
srv = None
|
||||
|
||||
if srv is not None:
|
||||
ensure_tables(srv)
|
||||
st = get_store(force_memory=force_memory or srv is None)
|
||||
ensure_tables(st)
|
||||
tid = tenant_id or "__platform__"
|
||||
seed = None
|
||||
if do_seed:
|
||||
ctx = TenantContext(tenant_id=tid, actor_type=ACTOR_SYSTEM, actor_id="seed",
|
||||
permissions={PERM_PLATFORM_ADMIN})
|
||||
seed = {"agents": seed_agents(ctx=ctx, store=st),
|
||||
"tools": seed_tools(ctx=ctx, store=st)}
|
||||
|
||||
ok, msgs = self_check()
|
||||
for m in msgs:
|
||||
_log(srv, m)
|
||||
if not ok:
|
||||
raise RuntimeError(
|
||||
"%s self_check FAILED(fail-closed,拒绝启动):%s"
|
||||
% (MODULE, "; ".join([m for m in msgs if "FAIL" in m or "应为" in m or "必须" in m][:6]))
|
||||
)
|
||||
if run_self_check:
|
||||
ok, msgs = self_check(tenant_id=tid, store=st)
|
||||
for m in msgs:
|
||||
_log(srv, m)
|
||||
if not ok:
|
||||
raise RuntimeError("%s self_check FAILED(fail-closed,拒绝启动):%s"
|
||||
% (MODULE_NAME,
|
||||
"; ".join([m for m in msgs if m.startswith("FAIL")][:8])))
|
||||
|
||||
if srv is not None:
|
||||
setattr(srv, "pbl_agent_tools", _registry.all_tools())
|
||||
setattr(srv, "pbl_agent_enabled_tools", [t["name"] for t in _registry.ENABLED_TOOLS])
|
||||
setattr(srv, "pbl_agent_disabled_tools", [t["name"] for t in _registry.DISABLED_TOOLS])
|
||||
setattr(srv, "pbl_agent_tools", list_tools(ctx=make_admin_ctx(tid), store=st))
|
||||
setattr(srv, "pbl_agent_enabled_tools", list(ENABLED_TOOL_CODES))
|
||||
setattr(srv, "pbl_agent_disabled_tools", list(DISABLED_TOOL_CODES))
|
||||
setattr(srv, "pbl_agent_api", api())
|
||||
setattr(srv, "pbl_agent_dbname", get_dbname(srv))
|
||||
setattr(srv, "pbl_agent_seed", seed)
|
||||
# dspy 处理函数注册(wwwroot/api/*.dspy 直接按名调用)
|
||||
setattr(srv, "pbl_agent_designer_run", pbl_agent_designer_run)
|
||||
setattr(srv, "pbl_agent_critic_run", pbl_agent_critic_run)
|
||||
setattr(srv, "pbl_tool_adjudicate", pbl_tool_adjudicate)
|
||||
setattr(srv, "pbl_agent_trace_list", pbl_agent_trace_list)
|
||||
setattr(srv, "pbl_agent_trace_write", pbl_agent_trace_write)
|
||||
setattr(srv, "pbl_approval_create", pbl_approval_create)
|
||||
setattr(srv, "pbl_approval_decide", pbl_approval_decide)
|
||||
setattr(srv, "pbl_approval_list", pbl_approval_list)
|
||||
modules = getattr(srv, "modules", None)
|
||||
if isinstance(modules, list) and MODULE not in modules:
|
||||
modules.append(MODULE)
|
||||
if isinstance(modules, list) and MODULE_NAME not in modules:
|
||||
modules.append(MODULE_NAME)
|
||||
|
||||
return _verdict.DEFAULT_VERDICT
|
||||
return DEFAULT_VERDICT
|
||||
|
||||
|
||||
def _log(srv, msg):
|
||||
logger = getattr(srv, "logger", None) if srv is not None else None
|
||||
if logger is not None and hasattr(logger, "info"):
|
||||
try:
|
||||
logger.info("[%s] %s" % (MODULE, msg))
|
||||
logger.info("[%s] %s" % (MODULE_NAME, msg))
|
||||
return
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
print("[%s] %s" % (MODULE, msg))
|
||||
print("[%s] %s" % (MODULE_NAME, msg))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,161 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_agent_runtime 表定义与建表(M4a)。
|
||||
|
||||
4 表(mariadb 方言,BIGINT AUTO_INCREMENT 主键,tenant_id 强制打头,无 FK/ENUM/TIMESTAMP):
|
||||
* pbl_agent_run —— Agent 运行主记录(designer/critic)
|
||||
* pbl_agent_step —— 运行步骤(think/tool/observe/plan/final)
|
||||
* pbl_tool_call —— 工具调用留痕(ALLOW/DENY 都落库,args 已脱敏)
|
||||
* pbl_tool_arbitration —— 裁决明细(九关链逐关结果)
|
||||
|
||||
对齐 docs/01-design/data-model.md 与 agent-tool-contract.md。
|
||||
"""
|
||||
|
||||
TABLES = ["pbl_agent_run", "pbl_agent_step", "pbl_tool_call", "pbl_tool_arbitration"]
|
||||
|
||||
DDL = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_run` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID(多租户强制打头)',
|
||||
`run_code` VARCHAR(64) NOT NULL COMMENT '运行编码',
|
||||
`agent_kind` VARCHAR(16) NOT NULL DEFAULT 'designer' COMMENT 'designer/critic',
|
||||
`goal` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '本轮目标',
|
||||
`blueprint_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联蓝图ID',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'running' COMMENT 'running/done/failed/aborted',
|
||||
`model` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '模型名',
|
||||
`step_count` INT NOT NULL DEFAULT 0 COMMENT '步骤数',
|
||||
`tool_call_count` INT NOT NULL DEFAULT 0 COMMENT '工具调用数',
|
||||
`tool_deny_count` INT NOT NULL DEFAULT 0 COMMENT '被拒工具调用数',
|
||||
`result` TEXT COMMENT '运行结果摘要',
|
||||
`error_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '错误码',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
`updated_at` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_run_code` (`tenant_id`, `run_code`),
|
||||
KEY `ix_tenant_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Agent 运行主记录'
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_step` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID(多租户强制打头)',
|
||||
`step_code` VARCHAR(64) NOT NULL COMMENT '步骤编码',
|
||||
`run_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属运行ID',
|
||||
`step_no` INT NOT NULL DEFAULT 0 COMMENT '步骤序号',
|
||||
`phase` VARCHAR(16) NOT NULL DEFAULT 'think' COMMENT 'think/tool/observe/plan/final',
|
||||
`content` TEXT COMMENT '步骤内容',
|
||||
`tokens_in` INT NOT NULL DEFAULT 0 COMMENT '输入 token',
|
||||
`tokens_out` INT NOT NULL DEFAULT 0 COMMENT '输出 token',
|
||||
`elapsed_ms` INT NOT NULL DEFAULT 0 COMMENT '耗时毫秒',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_run_step` (`tenant_id`, `run_id`, `step_no`),
|
||||
KEY `ix_tenant_phase` (`tenant_id`, `phase`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Agent 运行步骤'
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `pbl_tool_call` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID(多租户强制打头)',
|
||||
`call_code` VARCHAR(64) NOT NULL COMMENT '调用编码',
|
||||
`run_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属运行ID',
|
||||
`tool_name` VARCHAR(64) NOT NULL COMMENT '工具名',
|
||||
`args` TEXT COMMENT '入参(脱敏后)',
|
||||
`verdict` VARCHAR(8) NOT NULL DEFAULT 'DENY' COMMENT 'ALLOW/DENY',
|
||||
`verdict_id` BIGINT NOT NULL DEFAULT 0 COMMENT '裁决记录ID',
|
||||
`result` TEXT COMMENT '执行结果(截断)',
|
||||
`state` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/ok/failed/denied',
|
||||
`error_code` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '错误码',
|
||||
`elapsed_ms` INT NOT NULL DEFAULT 0 COMMENT '耗时毫秒',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '调用人',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_call_code` (`tenant_id`, `call_code`),
|
||||
KEY `ix_tenant_tool` (`tenant_id`, `tool_name`),
|
||||
KEY `ix_tenant_verdict` (`tenant_id`, `verdict`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具调用留痕'
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `pbl_tool_arbitration` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID(多租户强制打头)',
|
||||
`arb_code` VARCHAR(64) NOT NULL COMMENT '裁决编码',
|
||||
`call_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联调用ID',
|
||||
`tool_name` VARCHAR(64) NOT NULL COMMENT '工具名',
|
||||
`verdict` VARCHAR(8) NOT NULL DEFAULT 'DENY' COMMENT 'ALLOW/DENY',
|
||||
`failed_gate` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个未通过关卡',
|
||||
`checks` TEXT COMMENT '九关链逐关结果 JSON',
|
||||
`policy_version` VARCHAR(16) NOT NULL DEFAULT 'v1' COMMENT '策略版本',
|
||||
`role_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '调用角色',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_arb_code` (`tenant_id`, `arb_code`),
|
||||
KEY `ix_tenant_gate` (`tenant_id`, `failed_gate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具裁决明细'
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def get_ddl():
|
||||
"""返回本模块全部建表语句(list[str])。"""
|
||||
return [x.strip() for x in DDL]
|
||||
|
||||
|
||||
def ensure_tables(env=None, sor=None):
|
||||
"""幂等建表:CREATE TABLE IF NOT EXISTS,重复执行无副作用。
|
||||
|
||||
返回 (已确保表名列表, 失败列表)。
|
||||
"""
|
||||
ok, bad = [], []
|
||||
runner = sor
|
||||
if runner is None and env is not None:
|
||||
runner = getattr(env, "sor", None) or getattr(env, "db", None)
|
||||
for stmt in get_ddl():
|
||||
name = stmt.split("`")[1] if "`" in stmt else "?"
|
||||
try:
|
||||
if runner is not None and hasattr(runner, "sqlExe"):
|
||||
runner.sqlExe(stmt)
|
||||
elif env is not None and hasattr(env, "sqlExe"):
|
||||
env.sqlExe(stmt)
|
||||
else:
|
||||
# 无 DB 连接(如离线自检)时只校验语句形态,不静默声称已建表
|
||||
if "AUTO_INCREMENT" not in stmt or "tenant_id" not in stmt:
|
||||
raise ValueError("DDL 形态不合规:%s" % name)
|
||||
ok.append(name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
bad.append((name, str(exc)[:160]))
|
||||
return ok, bad
|
||||
|
||||
|
||||
def self_check():
|
||||
"""离线自检:4 表齐全、tenant_id 打头、无禁用方言。返回 (all_ok, msgs)。"""
|
||||
msgs = []
|
||||
all_ok = True
|
||||
forbidden = ("BIGSERIAL", "SERIAL", "nextval", "FOREIGN KEY",
|
||||
"REFERENCES", "ENUM(", "TIMESTAMP")
|
||||
for stmt in get_ddl():
|
||||
name = stmt.split("`")[1]
|
||||
if name not in TABLES:
|
||||
all_ok = False
|
||||
msgs.append("未知表 %s" % name)
|
||||
body = stmt.upper()
|
||||
for kw in forbidden:
|
||||
if kw in body:
|
||||
all_ok = False
|
||||
msgs.append("%s 命中禁用方言 %s" % (name, kw))
|
||||
cols = [ln.strip().split("`")[1] for ln in stmt.splitlines()
|
||||
if ln.strip().startswith("`")]
|
||||
biz = [c for c in cols if c != "id"]
|
||||
if not biz or biz[0] != "tenant_id":
|
||||
all_ok = False
|
||||
msgs.append("%s 首个业务列=%s(应为 tenant_id)" % (name, biz[:1]))
|
||||
if "AUTO_INCREMENT" not in body:
|
||||
all_ok = False
|
||||
msgs.append("%s 缺 AUTO_INCREMENT 主键" % name)
|
||||
if len(TABLES) != 4:
|
||||
all_ok = False
|
||||
msgs.append("表数=%d 应为 4" % len(TABLES))
|
||||
if all_ok:
|
||||
msgs.append("SELF_CHECK pbl_agent_runtime.tables: PASS %d/%d" % (len(TABLES), len(TABLES)))
|
||||
return all_ok, msgs
|
||||
@ -1,196 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Agent 工具注册表(M4a)—— 22 工具 = 13 启用 + 9 禁用,fail-closed。
|
||||
|
||||
对齐 docs/01-design/agent-tool-contract.md:
|
||||
* 未注册工具 → 一律 DENY(PBL-TOOL-0001)
|
||||
* 注册但 enabled=False → 一律 DENY(PBL-TOOL-0002)
|
||||
* 写保护域(rbac/world/scene/entity/scense/scense_runtime/script_engine 基表)
|
||||
出现 enabled 的写工具 → self_check 直接失败(启动即拒)
|
||||
|
||||
工具契约字段:
|
||||
name / kind(read|write) / domain / enabled / role(最低角色) /
|
||||
required(必填参数) / schema(参数类型与枚举) / handler(点分路径) / desc
|
||||
"""
|
||||
|
||||
WRITE_PROTECTED_DOMAINS = (
|
||||
"rbac", "world", "scene", "entity",
|
||||
"scense", "scense_runtime", "script_engine",
|
||||
)
|
||||
|
||||
POLICY_VERSION = "v1"
|
||||
|
||||
# ---- 13 个启用工具(Agent 可调用)
|
||||
ENABLED_TOOLS = [
|
||||
{"name": "blueprint.get", "kind": "read", "domain": "pbl_blueprint", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "blueprint_id"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int"},
|
||||
"handler": "pbl_blueprint.api:get_blueprint",
|
||||
"desc": "读取蓝图聚合根(租户隔离)"},
|
||||
{"name": "blueprint.tree", "kind": "read", "domain": "pbl_blueprint", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "blueprint_id"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int", "depth": "int"},
|
||||
"handler": "pbl_blueprint.api:get_tree",
|
||||
"desc": "读取蓝图子对象树"},
|
||||
{"name": "blueprint.patch", "kind": "write", "domain": "pbl_blueprint", "enabled": True,
|
||||
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id", "obj_type", "payload"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int", "payload": "dict",
|
||||
"obj_type": {"enum": ["goal", "role", "task", "artifact", "rubric",
|
||||
"resource", "flow"]}},
|
||||
"handler": "pbl_blueprint.api:patch_subobject",
|
||||
"desc": "泛化写入 7 类子对象"},
|
||||
{"name": "validation.run", "kind": "read", "domain": "pbl_validation", "enabled": True,
|
||||
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int", "ruleset_code": "str"},
|
||||
"handler": "pbl_validation.api:run_validation",
|
||||
"desc": "14 维校验 + 5 级质量状态"},
|
||||
{"name": "validation.findings", "kind": "read", "domain": "pbl_validation", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "run_id"],
|
||||
"schema": {"tenant_id": "int", "run_id": "int", "severity": {"enum": ["info", "warn", "error", "block"]}},
|
||||
"handler": "pbl_validation.api:list_findings",
|
||||
"desc": "读取校验发现项"},
|
||||
{"name": "compiler.compile", "kind": "write", "domain": "pbl_compiler", "enabled": True,
|
||||
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int", "target": {"enum": ["game_definition", "runtime_bundle"]}},
|
||||
"handler": "pbl_compiler.api:compile_blueprint",
|
||||
"desc": "确定性编译(同输入同输出)"},
|
||||
{"name": "compiler.artifact", "kind": "read", "domain": "pbl_compiler", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "job_id"],
|
||||
"schema": {"tenant_id": "int", "job_id": "int"},
|
||||
"handler": "pbl_compiler.api:get_artifact",
|
||||
"desc": "读取编译产物"},
|
||||
{"name": "evidence.collect", "kind": "write", "domain": "pbl_evidence", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "run_id", "idem_key", "payload"],
|
||||
"schema": {"tenant_id": "int", "run_id": "int", "idem_key": "str", "payload": "dict"},
|
||||
"handler": "pbl_evidence.api:collect_evidence",
|
||||
"desc": "幂等采集产出物证据"},
|
||||
{"name": "evidence.list", "kind": "read", "domain": "pbl_evidence", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "run_id"],
|
||||
"schema": {"tenant_id": "int", "run_id": "int", "kind": "str"},
|
||||
"handler": "pbl_evidence.api:list_evidence",
|
||||
"desc": "列出证据"},
|
||||
{"name": "assessment.score", "kind": "write", "domain": "pbl_assessment", "enabled": True,
|
||||
"role": "teacher.pbls", "required": ["tenant_id", "blueprint_id", "team_id"],
|
||||
"schema": {"tenant_id": "int", "blueprint_id": "int", "team_id": "int"},
|
||||
"handler": "pbl_assessment.api:score_rubric",
|
||||
"desc": "Rubric 加权评估"},
|
||||
{"name": "assessment.report", "kind": "read", "domain": "pbl_assessment", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "report_id"],
|
||||
"schema": {"tenant_id": "int", "report_id": "int"},
|
||||
"handler": "pbl_assessment.api:get_report",
|
||||
"desc": "读取评估报告"},
|
||||
{"name": "runtime.event_append", "kind": "write", "domain": "pbl_runtime_ext", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "world_id", "event_type", "payload"],
|
||||
"schema": {"tenant_id": "int", "world_id": "int", "event_type": "str", "payload": "dict"},
|
||||
"handler": "pbl_runtime_ext.api:append_event",
|
||||
"desc": "单事务事件+状态写入(薄扩展,不改基表)"},
|
||||
{"name": "kdb.query", "kind": "read", "domain": "pbl_kdb_ext", "enabled": True,
|
||||
"role": "student.pbls", "required": ["tenant_id", "query_code"],
|
||||
"schema": {"tenant_id": "int", "query_code": "str", "anon": "bool"},
|
||||
"handler": "pbl_kdb_ext.api:kdb_query",
|
||||
"desc": "KDB 只读桩 + 匿名聚合(零写入)"},
|
||||
]
|
||||
|
||||
# ---- 9 个禁用工具(注册但 enabled=False,调用一律 DENY)
|
||||
DISABLED_TOOLS = [
|
||||
{"name": "rbac.grant_role", "kind": "write", "domain": "rbac", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "user_id", "role_code"],
|
||||
"schema": {"tenant_id": "int", "user_id": "int", "role_code": "str"},
|
||||
"handler": "", "desc": "写保护域:禁止 Agent 授权(Phase 0/1 禁用)"},
|
||||
{"name": "rbac.revoke_role", "kind": "write", "domain": "rbac", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "user_id", "role_code"],
|
||||
"schema": {"tenant_id": "int", "user_id": "int", "role_code": "str"},
|
||||
"handler": "", "desc": "写保护域:禁止 Agent 撤权"},
|
||||
{"name": "world.delete", "kind": "write", "domain": "world", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "world_id"],
|
||||
"schema": {"tenant_id": "int", "world_id": "int"},
|
||||
"handler": "", "desc": "写保护域:禁止删除世界基表"},
|
||||
{"name": "scene.delete", "kind": "write", "domain": "scene", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "scene_id"],
|
||||
"schema": {"tenant_id": "int", "scene_id": "int"},
|
||||
"handler": "", "desc": "写保护域:禁止删除场景基表"},
|
||||
{"name": "entity.delete", "kind": "write", "domain": "entity", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "entity_id"],
|
||||
"schema": {"tenant_id": "int", "entity_id": "int"},
|
||||
"handler": "", "desc": "写保护域:禁止删除实体基表"},
|
||||
{"name": "script_engine.execute_raw", "kind": "write", "domain": "script_engine", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "script_text"],
|
||||
"schema": {"tenant_id": "int", "script_text": "str"},
|
||||
"handler": "", "desc": "写保护域:禁止执行任意脚本"},
|
||||
{"name": "scense.reset_session", "kind": "write", "domain": "scense", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "session_id"],
|
||||
"schema": {"tenant_id": "int", "session_id": "int"},
|
||||
"handler": "", "desc": "写保护域:禁止重置游戏会话"},
|
||||
{"name": "db.ddl_exec", "kind": "write", "domain": "platform", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id", "ddl"],
|
||||
"schema": {"tenant_id": "int", "ddl": "str"},
|
||||
"handler": "", "desc": "禁止 Agent 执行 DDL"},
|
||||
{"name": "tenant.switch", "kind": "write", "domain": "platform", "enabled": False,
|
||||
"role": "owner.pbls", "required": ["tenant_id"],
|
||||
"schema": {"tenant_id": "int"},
|
||||
"handler": "", "desc": "禁止 Agent 切换租户上下文(防跨租户)"},
|
||||
]
|
||||
|
||||
EXPECTED_ENABLED = 13
|
||||
EXPECTED_DISABLED = 9
|
||||
|
||||
|
||||
def all_tools():
|
||||
"""返回全部注册工具(启用 + 禁用),22 项。"""
|
||||
return list(ENABLED_TOOLS) + list(DISABLED_TOOLS)
|
||||
|
||||
|
||||
def registry():
|
||||
"""返回 name → tool 字典。"""
|
||||
return {t["name"]: t for t in all_tools()}
|
||||
|
||||
|
||||
def get_tool(name):
|
||||
"""按名取工具契约;未注册返回 None(调用方必须 fail-closed DENY)。"""
|
||||
return registry().get(name)
|
||||
|
||||
|
||||
def is_registered(name):
|
||||
return name in registry()
|
||||
|
||||
|
||||
def is_enabled(name):
|
||||
t = get_tool(name)
|
||||
return bool(t and t.get("enabled"))
|
||||
|
||||
|
||||
def self_check():
|
||||
"""数量契约 + 写保护域校验。返回 (all_ok, msgs)。
|
||||
|
||||
硬约束:
|
||||
* len(ENABLED_TOOLS) == 13 且 len(DISABLED_TOOLS) == 9(总 22)
|
||||
* 工具名全局唯一
|
||||
* 写保护域内不得存在 enabled=True 的 write 工具
|
||||
* 每个 enabled 工具必须有非空 handler
|
||||
"""
|
||||
msgs = []
|
||||
all_ok = True
|
||||
if len(ENABLED_TOOLS) != EXPECTED_ENABLED:
|
||||
all_ok = False
|
||||
msgs.append("启用工具数=%d 应为 %d" % (len(ENABLED_TOOLS), EXPECTED_ENABLED))
|
||||
if len(DISABLED_TOOLS) != EXPECTED_DISABLED:
|
||||
all_ok = False
|
||||
msgs.append("禁用工具数=%d 应为 %d" % (len(DISABLED_TOOLS), EXPECTED_DISABLED))
|
||||
names = [t["name"] for t in all_tools()]
|
||||
dup = sorted(set([n for n in names if names.count(n) > 1]))
|
||||
if dup:
|
||||
all_ok = False
|
||||
msgs.append("工具名重复:%s" % dup)
|
||||
for t in all_tools():
|
||||
if t.get("domain") in WRITE_PROTECTED_DOMAINS and t.get("enabled") and t.get("kind") == "write":
|
||||
all_ok = False
|
||||
msgs.append("写保护域 %s 存在启用写工具 %s" % (t["domain"], t["name"]))
|
||||
if t.get("enabled") and not t.get("handler"):
|
||||
all_ok = False
|
||||
msgs.append("启用工具 %s 缺 handler" % t["name"])
|
||||
if not t.get("required") or "tenant_id" not in t["required"]:
|
||||
all_ok = False
|
||||
msgs.append("工具 %s required 未含 tenant_id" % t["name"])
|
||||
if all_ok:
|
||||
msgs.append("SELF_CHECK pbl_agent_runtime.tool_registry: PASS %d/%d (enabled=%d disabled=%d)"
|
||||
% (len(names), len(names), len(ENABLED_TOOLS), len(DISABLED_TOOLS)))
|
||||
return all_ok, msgs
|
||||
@ -1,457 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""fail-closed 工具裁决引擎(M4b)。
|
||||
|
||||
九关链(任一关不过 → DENY,逐关 append checks{gate,pass,reason}):
|
||||
G1 registered PBL-TOOL-0001 工具未注册
|
||||
G2 enabled PBL-TOOL-0002 工具已注册但禁用
|
||||
G3 role PBL-PERM-0001 角色权限不足
|
||||
G4 tenant_present PBL-TENANT-0001 缺租户上下文
|
||||
G5 tenant_match PBL-TENANT-0002 跨租户访问
|
||||
G6 required PBL-PARAM-0001 缺必填参数
|
||||
G7 schema PBL-PARAM-0002 参数类型/枚举不合法(写工具未登记参数严格拒)
|
||||
G8 write_protected PBL-PERM-0002 写保护域写操作
|
||||
G9 quota PBL-TOOL-0005 超出单轮调用配额
|
||||
|
||||
铁律:
|
||||
* DEFAULT_VERDICT = 'DENY'(默认拒,非默认放)
|
||||
* ALLOW / DENY 都留痕(pbl_tool_call + pbl_tool_arbitration + write_audit)
|
||||
* 执行异常不降级为 ALLOW;state=failed 并转码
|
||||
* 敏感字段脱敏后落库;结果截断
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from . import tool_registry
|
||||
|
||||
POLICY_VERSION = "v1"
|
||||
DEFAULT_VERDICT = "DENY"
|
||||
MAX_CALLS_PER_RUN = 200
|
||||
MAX_RESULT_CHARS = 8000
|
||||
MAX_ARGS_CHARS = 4000
|
||||
|
||||
SENSITIVE_KEYS = (
|
||||
"password", "passwd", "pwd", "secret", "token", "api_key",
|
||||
"apikey", "credential", "id_card", "idcard", "phone", "mobile",
|
||||
"bank_account", "private_key",
|
||||
)
|
||||
|
||||
ROLE_LEVEL = {
|
||||
"observer.pbls": 0,
|
||||
"student.pbls": 1,
|
||||
"assistant.pbls": 2,
|
||||
"teacher.pbls": 3,
|
||||
"admin.pbls": 4,
|
||||
"owner.pbls": 5,
|
||||
"owner.audit": 4,
|
||||
}
|
||||
|
||||
ERR = {
|
||||
"unregistered": "PBL-TOOL-0001",
|
||||
"disabled": "PBL-TOOL-0002",
|
||||
"role": "PBL-PERM-0001",
|
||||
"tenant_missing": "PBL-TENANT-0001",
|
||||
"tenant_mismatch": "PBL-TENANT-0002",
|
||||
"required": "PBL-PARAM-0001",
|
||||
"schema": "PBL-PARAM-0002",
|
||||
"write_protected": "PBL-PERM-0002",
|
||||
"quota": "PBL-TOOL-0005",
|
||||
"handler_missing": "PBL-TOOL-0004",
|
||||
"exec_error": "PBL-EXEC-0001",
|
||||
}
|
||||
|
||||
|
||||
class PblError(Exception):
|
||||
"""业务错误(带错误码)。"""
|
||||
|
||||
def __init__(self, code, message):
|
||||
super(PblError, self).__init__(message)
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
def _redact(obj):
|
||||
"""递归脱敏敏感字段(8+ 类)。"""
|
||||
if isinstance(obj, dict):
|
||||
out = {}
|
||||
for k, v in obj.items():
|
||||
if isinstance(k, str) and any(s in k.lower() for s in SENSITIVE_KEYS):
|
||||
out[k] = "***REDACTED***"
|
||||
else:
|
||||
out[k] = _redact(v)
|
||||
return out
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_redact(x) for x in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def _dump(obj, limit=MAX_ARGS_CHARS):
|
||||
"""安全序列化 + 截断。"""
|
||||
try:
|
||||
text = json.dumps(obj, ensure_ascii=False, default=str)
|
||||
except (TypeError, ValueError):
|
||||
text = str(obj)
|
||||
if len(text) > limit:
|
||||
text = text[:limit] + "...[TRUNCATED %d]" % len(text)
|
||||
return text
|
||||
|
||||
|
||||
def _new_code(prefix):
|
||||
return "%s%s" % (prefix, uuid.uuid4().hex[:16])
|
||||
|
||||
|
||||
def _check(gate, passed, reason=""):
|
||||
return {"gate": gate, "pass": bool(passed), "reason": reason}
|
||||
|
||||
|
||||
def _coerce(value, spec, strict_write):
|
||||
"""按 schema 校验/转换单个参数。返回 (ok, converted, reason)。"""
|
||||
if isinstance(spec, dict) and "enum" in spec:
|
||||
allowed = spec["enum"]
|
||||
if value not in allowed:
|
||||
return False, value, "值 %r 不在枚举 %s" % (value, allowed)
|
||||
return True, value, ""
|
||||
t = spec if isinstance(spec, str) else "any"
|
||||
if t == "int":
|
||||
try:
|
||||
return True, int(value), ""
|
||||
except (TypeError, ValueError):
|
||||
return False, value, "期望 int,实得 %r" % (value,)
|
||||
if t == "float":
|
||||
try:
|
||||
return True, float(value), ""
|
||||
except (TypeError, ValueError):
|
||||
return False, value, "期望 float,实得 %r" % (value,)
|
||||
if t == "bool":
|
||||
if isinstance(value, bool):
|
||||
return True, value, ""
|
||||
if isinstance(value, str) and value.lower() in ("true", "false", "1", "0"):
|
||||
return True, value.lower() in ("true", "1"), ""
|
||||
return False, value, "期望 bool,实得 %r" % (value,)
|
||||
if t == "str":
|
||||
if not isinstance(value, str):
|
||||
if strict_write:
|
||||
return False, value, "期望 str,实得 %r" % (value,)
|
||||
return True, str(value), ""
|
||||
return True, value, ""
|
||||
if t == "dict":
|
||||
if not isinstance(value, dict):
|
||||
return False, value, "期望 dict,实得 %r" % (value,)
|
||||
return True, value, ""
|
||||
if t == "list":
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return False, value, "期望 list,实得 %r" % (value,)
|
||||
return True, list(value), ""
|
||||
return True, value, ""
|
||||
|
||||
|
||||
def judge(tool_name, args, ctx, quota_used=0):
|
||||
"""九关链裁决。返回 dict:
|
||||
{verdict, error_code, failed_gate, checks, tool, args(脱敏转换后), call_id}
|
||||
"""
|
||||
args = dict(args or {})
|
||||
ctx = dict(ctx or {})
|
||||
checks = []
|
||||
safe_args = _redact(args)
|
||||
|
||||
result = {
|
||||
"verdict": DEFAULT_VERDICT,
|
||||
"error_code": "",
|
||||
"failed_gate": "",
|
||||
"checks": checks,
|
||||
"tool": tool_name,
|
||||
"args": safe_args,
|
||||
"policy_version": POLICY_VERSION,
|
||||
"call_id": _new_code("call_"),
|
||||
"arb_code": _new_code("arb_"),
|
||||
}
|
||||
|
||||
def deny(gate, code, reason):
|
||||
result["failed_gate"] = gate
|
||||
result["error_code"] = code
|
||||
result["verdict"] = DEFAULT_VERDICT
|
||||
checks.append(_check(gate, False, reason))
|
||||
return result
|
||||
|
||||
# G1 registered
|
||||
tool = tool_registry.get_tool(tool_name)
|
||||
if tool is None:
|
||||
return deny("G1_registered", ERR["unregistered"],
|
||||
"工具 %r 未注册(fail-closed)" % tool_name)
|
||||
checks.append(_check("G1_registered", True, ""))
|
||||
|
||||
# G2 enabled
|
||||
if not tool.get("enabled"):
|
||||
return deny("G2_enabled", ERR["disabled"],
|
||||
"工具 %r 已注册但禁用(%s)" % (tool_name, tool.get("desc", "")))
|
||||
checks.append(_check("G2_enabled", True, ""))
|
||||
|
||||
# G3 role
|
||||
role = ctx.get("role") or ""
|
||||
need = tool.get("role") or "student.pbls"
|
||||
if ROLE_LEVEL.get(role, -1) < ROLE_LEVEL.get(need, 99):
|
||||
return deny("G3_role", ERR["role"],
|
||||
"角色 %r 权限不足,需 %r" % (role, need))
|
||||
checks.append(_check("G3_role", True, "role=%s need=%s" % (role, need)))
|
||||
|
||||
# G4 tenant_present
|
||||
ctx_tenant = ctx.get("tenant_id")
|
||||
if ctx_tenant in (None, "", 0, "0"):
|
||||
return deny("G4_tenant_present", ERR["tenant_missing"], "缺租户上下文 tenant_id")
|
||||
checks.append(_check("G4_tenant_present", True, "tenant_id=%s" % ctx_tenant))
|
||||
|
||||
# G5 tenant_match(防跨租户)
|
||||
arg_tenant = args.get("tenant_id")
|
||||
if arg_tenant is not None and str(arg_tenant) != str(ctx_tenant):
|
||||
return deny("G5_tenant_match", ERR["tenant_mismatch"],
|
||||
"参数 tenant_id=%r 与上下文 %r 不一致" % (arg_tenant, ctx_tenant))
|
||||
args["tenant_id"] = int(ctx_tenant) if str(ctx_tenant).lstrip("-").isdigit() else ctx_tenant
|
||||
checks.append(_check("G5_tenant_match", True, ""))
|
||||
|
||||
# G6 required
|
||||
missing = [k for k in (tool.get("required") or []) if k not in args or args[k] in (None, "")]
|
||||
if missing:
|
||||
return deny("G6_required", ERR["required"], "缺必填参数 %s" % missing)
|
||||
checks.append(_check("G6_required", True, ""))
|
||||
|
||||
# G7 schema(写工具未登记参数严格拒)
|
||||
schema = tool.get("schema") or {}
|
||||
strict_write = tool.get("kind") == "write"
|
||||
unknown = [k for k in args if k not in schema]
|
||||
if strict_write and unknown:
|
||||
return deny("G7_schema", ERR["schema"],
|
||||
"写工具存在未登记参数 %s(严格拒)" % unknown)
|
||||
converted = {}
|
||||
for k, v in args.items():
|
||||
spec = schema.get(k, "any")
|
||||
ok, val, reason = _coerce(v, spec, strict_write)
|
||||
if not ok:
|
||||
return deny("G7_schema", ERR["schema"], "参数 %s %s" % (k, reason))
|
||||
converted[k] = val
|
||||
checks.append(_check("G7_schema", True, ""))
|
||||
|
||||
# G8 write_protected
|
||||
if tool.get("kind") == "write" and tool.get("domain") in tool_registry.WRITE_PROTECTED_DOMAINS:
|
||||
return deny("G8_write_protected", ERR["write_protected"],
|
||||
"写保护域 %s 禁止写操作" % tool.get("domain"))
|
||||
checks.append(_check("G8_write_protected", True, "domain=%s" % tool.get("domain")))
|
||||
|
||||
# G9 quota
|
||||
if quota_used >= MAX_CALLS_PER_RUN:
|
||||
return deny("G9_quota", ERR["quota"],
|
||||
"单轮调用已达上限 %d" % MAX_CALLS_PER_RUN)
|
||||
checks.append(_check("G9_quota", True, "used=%d/%d" % (quota_used, MAX_CALLS_PER_RUN)))
|
||||
|
||||
result["verdict"] = "ALLOW"
|
||||
result["args"] = _redact(converted)
|
||||
result["_converted"] = converted
|
||||
return result
|
||||
|
||||
|
||||
def _persist_verdict(env, verdict_rec, state, result_text, error_code, elapsed_ms):
|
||||
"""ALLOW/DENY 都留痕:pbl_tool_call + pbl_tool_arbitration + write_audit。"""
|
||||
written = []
|
||||
if env is None:
|
||||
return written
|
||||
tenant_id = (verdict_rec.get("args") or {}).get("tenant_id") or 0
|
||||
sor = getattr(env, "sor", None)
|
||||
call_row = {
|
||||
"tenant_id": tenant_id,
|
||||
"call_code": verdict_rec["call_id"],
|
||||
"tool_name": verdict_rec["tool"],
|
||||
"args": _dump(verdict_rec.get("args"), MAX_ARGS_CHARS),
|
||||
"verdict": verdict_rec["verdict"],
|
||||
"result": _dump(result_text, MAX_RESULT_CHARS),
|
||||
"state": state,
|
||||
"error_code": error_code or "",
|
||||
"elapsed_ms": int(elapsed_ms),
|
||||
}
|
||||
arb_row = {
|
||||
"tenant_id": tenant_id,
|
||||
"arb_code": verdict_rec["arb_code"],
|
||||
"tool_name": verdict_rec["tool"],
|
||||
"verdict": verdict_rec["verdict"],
|
||||
"failed_gate": verdict_rec.get("failed_gate") or "",
|
||||
"checks": _dump(verdict_rec.get("checks"), MAX_ARGS_CHARS),
|
||||
"policy_version": POLICY_VERSION,
|
||||
"role_code": "",
|
||||
}
|
||||
for tbl, row in (("pbl_tool_call", call_row), ("pbl_tool_arbitration", arb_row)):
|
||||
try:
|
||||
if sor is not None and hasattr(sor, "C"):
|
||||
sor.C(tbl, row)
|
||||
written.append(tbl)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
try:
|
||||
audit = getattr(env, "write_audit", None)
|
||||
if callable(audit):
|
||||
audit("tool_call" if verdict_rec["verdict"] == "ALLOW" else "tool_deny",
|
||||
{"tool": verdict_rec["tool"], "gate": verdict_rec.get("failed_gate"),
|
||||
"code": error_code, "tenant_id": tenant_id})
|
||||
written.append("audit")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return written
|
||||
|
||||
|
||||
def _resolve_handler(dotted):
|
||||
"""'pkg.mod:func' → callable。解析失败抛 PblError(不降级)。"""
|
||||
if not dotted or ":" not in dotted:
|
||||
raise PblError(ERR["handler_missing"], "handler 未登记:%r" % dotted)
|
||||
mod_path, fn_name = dotted.split(":", 1)
|
||||
try:
|
||||
mod = importlib.import_module(mod_path)
|
||||
except ImportError as exc:
|
||||
raise PblError(ERR["handler_missing"], "导入 %s 失败:%s" % (mod_path, exc))
|
||||
fn = getattr(mod, fn_name, None)
|
||||
if not callable(fn):
|
||||
raise PblError(ERR["handler_missing"], "%s 无 %s()" % (mod_path, fn_name))
|
||||
return fn
|
||||
|
||||
|
||||
def call_tool(env, tool_name, args, ctx, quota_used=0):
|
||||
"""裁决 → 执行 → 留痕。返回 dict{verdict,state,result,error_code,call_id,elapsed_ms}。
|
||||
|
||||
执行异常分别转码:PblError 用其 code;TypeError/ValueError → PBL-PARAM-0002;
|
||||
其它 Exception → PBL-EXEC-0001。任何异常都不降级为 ALLOW。
|
||||
"""
|
||||
t0 = time.time()
|
||||
rec = judge(tool_name, args, ctx, quota_used=quota_used)
|
||||
out = {
|
||||
"call_id": rec["call_id"],
|
||||
"arb_code": rec["arb_code"],
|
||||
"tool": tool_name,
|
||||
"verdict": rec["verdict"],
|
||||
"state": "denied" if rec["verdict"] != "ALLOW" else "pending",
|
||||
"result": None,
|
||||
"error_code": rec.get("error_code") or "",
|
||||
"failed_gate": rec.get("failed_gate") or "",
|
||||
"checks": rec["checks"],
|
||||
"elapsed_ms": 0,
|
||||
}
|
||||
if rec["verdict"] != "ALLOW":
|
||||
out["elapsed_ms"] = int((time.time() - t0) * 1000)
|
||||
_persist_verdict(env, rec, "denied", None, out["error_code"], out["elapsed_ms"])
|
||||
return out
|
||||
|
||||
tool = tool_registry.get_tool(tool_name) or {}
|
||||
try:
|
||||
fn = _resolve_handler(tool.get("handler"))
|
||||
payload = rec.get("_converted") or rec["args"]
|
||||
res = fn(**payload)
|
||||
out["state"] = "ok"
|
||||
out["result"] = res
|
||||
except PblError as exc:
|
||||
out["state"] = "failed"
|
||||
out["error_code"] = exc.code
|
||||
out["result"] = {"message": exc.message}
|
||||
except (TypeError, ValueError) as exc:
|
||||
out["state"] = "failed"
|
||||
out["error_code"] = ERR["schema"]
|
||||
out["result"] = {"message": str(exc)[:400]}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
out["state"] = "failed"
|
||||
out["error_code"] = ERR["exec_error"]
|
||||
out["result"] = {"message": str(exc)[:400]}
|
||||
out["elapsed_ms"] = int((time.time() - t0) * 1000)
|
||||
_persist_verdict(env, rec, out["state"], out["result"], out["error_code"], out["elapsed_ms"])
|
||||
return out
|
||||
|
||||
|
||||
def verdict_stats(env=None, rows=None):
|
||||
"""统计 allow/deny/deny_rate/deny_by_gate。rows 可外部注入(离线自检用)。"""
|
||||
rows = rows if rows is not None else []
|
||||
total = len(rows)
|
||||
allow = len([r for r in rows if (r.get("verdict") or "").upper() == "ALLOW"])
|
||||
deny = total - allow
|
||||
by_gate = {}
|
||||
for r in rows:
|
||||
g = r.get("failed_gate") or ""
|
||||
if g:
|
||||
by_gate[g] = by_gate.get(g, 0) + 1
|
||||
return {
|
||||
"total": total,
|
||||
"allow": allow,
|
||||
"deny": deny,
|
||||
"deny_rate": round(float(deny) / total, 4) if total else 0.0,
|
||||
"deny_by_gate": by_gate,
|
||||
"policy_version": POLICY_VERSION,
|
||||
}
|
||||
|
||||
|
||||
def runtime_self_check():
|
||||
"""6 探针:5 个必须 DENY + 1 个合法读必须 ALLOW。返回 (all_ok, msgs)。"""
|
||||
msgs = []
|
||||
all_ok = True
|
||||
base_ctx = {"tenant_id": 7, "role": "student.pbls"}
|
||||
probes = [
|
||||
("未知工具必须 DENY", "no.such_tool", {}, base_ctx, "DENY", "G1_registered"),
|
||||
("禁用工具必须 DENY", "rbac.grant_role", {"tenant_id": 7, "user_id": 1, "role_code": "admin.pbls"},
|
||||
dict(base_ctx, role="owner.pbls"), "DENY", "G2_enabled"),
|
||||
("缺 tenant 必须 DENY", "blueprint.get", {"blueprint_id": 1},
|
||||
{"role": "student.pbls"}, "DENY", "G4_tenant_present"),
|
||||
("student 调写工具必须 DENY", "blueprint.patch",
|
||||
{"tenant_id": 7, "blueprint_id": 1, "obj_type": "goal", "payload": {}},
|
||||
base_ctx, "DENY", "G3_role"),
|
||||
("缺必填必须 DENY", "blueprint.get", {"tenant_id": 7}, base_ctx, "DENY", "G6_required"),
|
||||
("跨租户必须 DENY", "blueprint.get", {"tenant_id": 8, "blueprint_id": 1},
|
||||
base_ctx, "DENY", "G5_tenant_match"),
|
||||
]
|
||||
for title, name, args, ctx, want, want_gate in probes:
|
||||
rec = judge(name, args, ctx)
|
||||
got = rec["verdict"]
|
||||
if got != want or (want == "DENY" and rec.get("failed_gate") != want_gate):
|
||||
all_ok = False
|
||||
msgs.append("探针[%s] FAIL:verdict=%s gate=%s(期望 %s/%s)"
|
||||
% (title, got, rec.get("failed_gate"), want, want_gate))
|
||||
else:
|
||||
msgs.append("探针[%s] PASS verdict=%s gate=%s" % (title, got, rec.get("failed_gate") or "-"))
|
||||
|
||||
# 合法读必须 ALLOW(teacher 角色读 blueprint.get)
|
||||
rec = judge("blueprint.get", {"tenant_id": 7, "blueprint_id": 1},
|
||||
{"tenant_id": 7, "role": "teacher.pbls"})
|
||||
if rec["verdict"] != "ALLOW":
|
||||
all_ok = False
|
||||
msgs.append("探针[合法读必须 ALLOW] FAIL:%s/%s" % (rec["verdict"], rec.get("failed_gate")))
|
||||
else:
|
||||
msgs.append("探针[合法读必须 ALLOW] PASS 九关全过")
|
||||
|
||||
# 配额关
|
||||
rec = judge("blueprint.get", {"tenant_id": 7, "blueprint_id": 1},
|
||||
{"tenant_id": 7, "role": "teacher.pbls"}, quota_used=MAX_CALLS_PER_RUN)
|
||||
if rec["verdict"] != "DENY" or rec.get("failed_gate") != "G9_quota":
|
||||
all_ok = False
|
||||
msgs.append("探针[配额超限必须 DENY] FAIL:%s/%s" % (rec["verdict"], rec.get("failed_gate")))
|
||||
else:
|
||||
msgs.append("探针[配额超限必须 DENY] PASS gate=G9_quota")
|
||||
|
||||
# 写保护域写工具必须 DENY(即使 owner 角色)
|
||||
wp = [t["name"] for t in tool_registry.all_tools()
|
||||
if t.get("kind") == "write" and t.get("domain") in tool_registry.WRITE_PROTECTED_DOMAINS]
|
||||
for name in wp:
|
||||
rec = judge(name, {"tenant_id": 7}, {"tenant_id": 7, "role": "owner.pbls"})
|
||||
if rec["verdict"] != "DENY":
|
||||
all_ok = False
|
||||
msgs.append("写保护域工具 %s 未被拒(verdict=%s)" % (name, rec["verdict"]))
|
||||
if all_ok:
|
||||
msgs.append("写保护域 %d 个写工具全部 DENY" % len(wp))
|
||||
|
||||
st = verdict_stats(rows=[{"verdict": "ALLOW"}, {"verdict": "DENY", "failed_gate": "G3_role"}])
|
||||
if st["total"] != 2 or st["deny_rate"] != 0.5 or st["deny_by_gate"].get("G3_role") != 1:
|
||||
all_ok = False
|
||||
msgs.append("verdict_stats 口径错误:%s" % st)
|
||||
else:
|
||||
msgs.append("verdict_stats 口径正确:%s" % st)
|
||||
|
||||
reg_ok, reg_msgs = tool_registry.self_check()
|
||||
if not reg_ok:
|
||||
all_ok = False
|
||||
msgs.extend(reg_msgs)
|
||||
|
||||
if all_ok:
|
||||
msgs.append("SELF_CHECK pbl_agent_runtime.verdict: PASS (default=%s, max_calls=%d, policy=%s)"
|
||||
% (DEFAULT_VERDICT, MAX_CALLS_PER_RUN, POLICY_VERSION))
|
||||
return all_ok, msgs
|
||||
@ -1 +1,28 @@
|
||||
打包元数据(352B):依赖 pbl_common + pbl_blueprint
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pbl_agent_runtime"
|
||||
version = "1.0.0"
|
||||
description = "PBL Designer/Critic Agent 运行时与 fail-closed 工具裁决(M4a:Agent 定义 + 13/9 工具注册 + 8 步裁决 + 四类强制人工审批)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
license = { text = "Proprietary" }
|
||||
authors = [{ name = "pbls dev team" }]
|
||||
keywords = ["pbl", "agent-runtime", "designer", "critic", "fail-closed", "tool-adjudication"]
|
||||
|
||||
# 说明:pbl_common / pbl_blueprint 为本机构 modules/ 下的基础包,
|
||||
# 由宿主应用通过 load_{module}() 装配,不在 PyPI 发布,故不列入 dependencies
|
||||
# (列入会导致 pip install 解析失败)。运行期无第三方 PyPI 依赖。
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=7.0"]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pbl_agent_runtime"]
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
pbl_agent_runtime = ["../sql/*.sql"]
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
-- pbl_agent_runtime 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_def` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "Agent编码",
|
||||
`agent_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`name` VARCHAR(128) NOT NULL COMMENT "名称",
|
||||
`model_route` VARCHAR(64) NOT NULL COMMENT "pipeline-llm 路由",
|
||||
`system_prompt_txt` TEXT NULL COMMENT "提示词",
|
||||
`tool_whitelist_json` LONGTEXT NULL COMMENT "工具白名单",
|
||||
`permission_mode` VARCHAR(64) NOT NULL COMMENT "read/write 模式",
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ad_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 定义(Designer/Critic)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_tool_registry` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`agent_scope` VARCHAR(64) NOT NULL COMMENT "作用域",
|
||||
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`params_schema_json` LONGTEXT NULL COMMENT "参数白名单Schema",
|
||||
`disabled_reason` VARCHAR(64) NOT NULL COMMENT "禁用原因",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tr_key` (`tenant_id`, `tool_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具注册表(13 启用 / 9 禁用含 pbl.publish)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_trace` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`trace_uid` VARCHAR(32) NOT NULL COMMENT "轨迹UID",
|
||||
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "目标蓝图",
|
||||
`input_ref_json` LONGTEXT NULL COMMENT "要素1 输入引用",
|
||||
`thought_txt` TEXT NULL COMMENT "要素2 思考",
|
||||
`action_code` VARCHAR(64) NOT NULL COMMENT "要素3 动作",
|
||||
`action_params_json` LONGTEXT NULL COMMENT "要素4 动作参数",
|
||||
`observation_json` LONGTEXT NULL COMMENT "要素5 观察",
|
||||
`result_state` VARCHAR(64) NOT NULL COMMENT "要素6 结果状态",
|
||||
`llm_calls` INT NOT NULL DEFAULT 0 COMMENT "要素7 LLM调用数",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "触发用户",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_at_uid` (`tenant_id`, `trace_uid`),
|
||||
KEY `idx_at_tenant` (`tenant_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 执行轨迹(第28章 7 要素)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_tool_call` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`trace_id` BIGINT UNSIGNED NOT NULL COMMENT "轨迹ID",
|
||||
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
|
||||
`request_json` LONGTEXT NULL COMMENT "请求",
|
||||
`response_json` LONGTEXT NULL COMMENT "响应",
|
||||
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
|
||||
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
|
||||
`adjudication_code` VARCHAR(64) NOT NULL COMMENT "裁决结论",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tc_trace` (`tenant_id`, `trace_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具调用明细(服务端裁决留痕)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_approval` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`approval_uid` VARCHAR(32) NOT NULL COMMENT "审批UID",
|
||||
`approval_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`target_type` VARCHAR(64) NOT NULL COMMENT "对象类型",
|
||||
`target_id` BIGINT UNSIGNED NOT NULL COMMENT "对象ID",
|
||||
`payload_json` LONGTEXT NULL COMMENT "变更内容",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`requested_by` VARCHAR(32) NOT NULL COMMENT "发起人",
|
||||
`decided_by` VARCHAR(32) NOT NULL COMMENT "决策人",
|
||||
`decided_at` DATETIME NULL NULL COMMENT "决策时间",
|
||||
`comment_txt` TEXT NULL COMMENT "意见",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ap_uid` (`tenant_id`, `approval_uid`),
|
||||
KEY `idx_ap_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="人工审批单(14.2 四类,无绕过路径)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_llm_call_log` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`call_uid` VARCHAR(32) NOT NULL COMMENT "调用UID",
|
||||
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
|
||||
`route` VARCHAR(64) NOT NULL COMMENT "路由",
|
||||
`model` VARCHAR(64) NOT NULL COMMENT "实际模型",
|
||||
`prompt_hash` VARCHAR(32) NOT NULL COMMENT "提示词哈希",
|
||||
`completion_tokens` INT NOT NULL DEFAULT 0 COMMENT "tokens",
|
||||
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
|
||||
`fallback_used` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "走兜底",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_lc_uid` (`tenant_id`, `call_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="LLM 调用日志(超时/重试/限流/兜底留痕)";
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
# pbl_agent_runtime/api/pbl_tool_registry_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_agent_runtime/api/pbl_tool_registry_list.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_tool_registry_list(**params_kw)
|
||||
return data
|
||||
@ -1,4 +0,0 @@
|
||||
# pbl_agent_runtime/api/pbl_tool_registry_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_agent_runtime/api/pbl_tool_registry_save.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_tool_registry_save(**params_kw)
|
||||
return data
|
||||
Loading…
x
Reference in New Issue
Block a user