diff --git a/json/agent_runtime_pbl_agent_def.json b/json/agent_runtime_pbl_agent_def.json deleted file mode 100644 index efea053..0000000 --- a/json/agent_runtime_pbl_agent_def.json +++ /dev/null @@ -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 - } -} diff --git a/json/agent_runtime_pbl_agent_tool_call.json b/json/agent_runtime_pbl_agent_tool_call.json deleted file mode 100644 index 50042cd..0000000 --- a/json/agent_runtime_pbl_agent_tool_call.json +++ /dev/null @@ -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 - } -} diff --git a/json/agent_runtime_pbl_agent_trace.json b/json/agent_runtime_pbl_agent_trace.json deleted file mode 100644 index 54c4a4d..0000000 --- a/json/agent_runtime_pbl_agent_trace.json +++ /dev/null @@ -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 - } -} diff --git a/json/agent_runtime_pbl_approval.json b/json/agent_runtime_pbl_approval.json deleted file mode 100644 index 3e54e95..0000000 --- a/json/agent_runtime_pbl_approval.json +++ /dev/null @@ -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 - } -} diff --git a/json/agent_runtime_pbl_llm_call_log.json b/json/agent_runtime_pbl_llm_call_log.json deleted file mode 100644 index 1fc097b..0000000 --- a/json/agent_runtime_pbl_llm_call_log.json +++ /dev/null @@ -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 - } -} diff --git a/json/agent_runtime_pbl_tool_registry.json b/json/agent_runtime_pbl_tool_registry.json deleted file mode 100644 index a6722a9..0000000 --- a/json/agent_runtime_pbl_tool_registry.json +++ /dev/null @@ -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 - } -} diff --git a/models/pbl_agent_def.json b/models/pbl_agent_def.json deleted file mode 100644 index 6c23e6d..0000000 --- a/models/pbl_agent_def.json +++ /dev/null @@ -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": [] -} diff --git a/models/pbl_agent_tool_call.json b/models/pbl_agent_tool_call.json deleted file mode 100644 index 5ef5200..0000000 --- a/models/pbl_agent_tool_call.json +++ /dev/null @@ -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": [] -} diff --git a/models/pbl_agent_trace.json b/models/pbl_agent_trace.json deleted file mode 100644 index 1afc3a0..0000000 --- a/models/pbl_agent_trace.json +++ /dev/null @@ -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": [] -} diff --git a/models/pbl_approval.json b/models/pbl_approval.json deleted file mode 100644 index b527b93..0000000 --- a/models/pbl_approval.json +++ /dev/null @@ -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" - } - ] -} diff --git a/models/pbl_llm_call_log.json b/models/pbl_llm_call_log.json deleted file mode 100644 index 6ecedf7..0000000 --- a/models/pbl_llm_call_log.json +++ /dev/null @@ -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": [] -} diff --git a/models/pbl_tool_registry.json b/models/pbl_tool_registry.json deleted file mode 100644 index b024a41..0000000 --- a/models/pbl_tool_registry.json +++ /dev/null @@ -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": [] -} diff --git a/pbl_agent_runtime/__init__.py b/pbl_agent_runtime/__init__.py index 6ff9149..629546d 100644 --- a/pbl_agent_runtime/__init__.py +++ b/pbl_agent_runtime/__init__.py @@ -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" diff --git a/pbl_agent_runtime/api.py b/pbl_agent_runtime/api.py deleted file mode 100644 index 20144a4..0000000 --- a/pbl_agent_runtime/api.py +++ /dev/null @@ -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 diff --git a/pbl_agent_runtime/init.py b/pbl_agent_runtime/init.py index e94966d..c0787ac 100644 --- a/pbl_agent_runtime/init.py +++ b/pbl_agent_runtime/init.py @@ -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__": diff --git a/pbl_agent_runtime/tables.py b/pbl_agent_runtime/tables.py deleted file mode 100644 index 5b97ebb..0000000 --- a/pbl_agent_runtime/tables.py +++ /dev/null @@ -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 diff --git a/pbl_agent_runtime/tool_registry.py b/pbl_agent_runtime/tool_registry.py deleted file mode 100644 index 1b78613..0000000 --- a/pbl_agent_runtime/tool_registry.py +++ /dev/null @@ -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 diff --git a/pbl_agent_runtime/verdict.py b/pbl_agent_runtime/verdict.py deleted file mode 100644 index 7f98adf..0000000 --- a/pbl_agent_runtime/verdict.py +++ /dev/null @@ -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 diff --git a/pyproject.toml b/pyproject.toml index b4a61c3..22dfb0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1 +1,28 @@ -打包元数据(352B):依赖 pbl_common + pbl_blueprint \ No newline at end of file +[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"] diff --git a/sql/pbl_agent_runtime.sql b/sql/pbl_agent_runtime.sql deleted file mode 100644 index 4e1e908..0000000 --- a/sql/pbl_agent_runtime.sql +++ /dev/null @@ -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 调用日志(超时/重试/限流/兜底留痕)"; - diff --git a/wwwroot/api/pbl_tool_registry_list.dspy b/wwwroot/api/pbl_tool_registry_list.dspy deleted file mode 100644 index 316b76f..0000000 --- a/wwwroot/api/pbl_tool_registry_list.dspy +++ /dev/null @@ -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 diff --git a/wwwroot/api/pbl_tool_registry_save.dspy b/wwwroot/api/pbl_tool_registry_save.dspy deleted file mode 100644 index 8b4a082..0000000 --- a/wwwroot/api/pbl_tool_registry_save.dspy +++ /dev/null @@ -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